├── .DS_Store ├── .gitignore ├── Github ├── Cover.png └── MinimalOnboarding.png ├── OnboardingKit.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm │ │ └── Package.resolved └── xcshareddata │ └── xcschemes │ └── OnboardingKit.xcscheme ├── OnboardingKit ├── .DS_Store ├── Assets.xcassets │ ├── .DS_Store │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── icon_1024.png │ │ ├── icon_120.png │ │ ├── icon_152.png │ │ ├── icon_167.png │ │ ├── icon_180.png │ │ ├── icon_20.png │ │ ├── icon_29.png │ │ ├── icon_40.png │ │ ├── icon_58.png │ │ ├── icon_60.png │ │ ├── icon_76.png │ │ ├── icon_80.png │ │ └── icon_87.png │ ├── BackgroundGrey.colorset │ │ └── Contents.json │ ├── Contents.json │ ├── DynamicWhite.colorset │ │ └── Contents.json │ └── Header.imageset │ │ ├── Contents.json │ │ └── Header.pdf ├── ContentView.swift ├── Info.plist ├── OnboardingKitApp.swift ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json └── Supporting Files │ └── .DS_Store ├── Packages ├── DesignHelpKit │ ├── .gitignore │ ├── Package.swift │ ├── README.md │ ├── Sources │ │ └── DesignHelpKit │ │ │ ├── FontDisplayable.swift │ │ │ └── UIImage+Module.swift │ └── Tests │ │ └── DesignHelpKitTests │ │ └── DesignHelpKitTests.swift └── MinimalOnboarding │ ├── .gitignore │ ├── Package.swift │ ├── README.md │ ├── Sources │ └── MinimalOnboarding │ │ ├── Dependencies │ │ ├── AuthService.swift │ │ ├── ContactValidator.swift │ │ ├── LinkParser.swift │ │ ├── NotificationPermissionProvider.swift │ │ └── UserService.swift │ │ ├── Design │ │ ├── Color.swift │ │ ├── Components │ │ │ ├── Header.swift │ │ │ ├── MinimalButton.swift │ │ │ └── Title.swift │ │ ├── ConfirmationCodePage.swift │ │ ├── DesignConstants.swift │ │ ├── MinimalFont.swift │ │ ├── NotificationPermissionPage.swift │ │ ├── SetupConfirmationPage.swift │ │ ├── Transition.swift │ │ └── Typography.swift │ │ ├── MinimalThemeContainer.swift │ │ ├── OnboardingFlow.swift │ │ ├── OnboardingFlowModel.swift │ │ ├── Pages │ │ ├── ConfirmEmailPage.swift │ │ ├── EmailPage.swift │ │ ├── IntroPage.swift │ │ ├── PhoneNumberEntryPage.swift │ │ └── PromotionPage.swift │ │ ├── Resources │ │ ├── Assets.xcassets │ │ │ ├── Colors │ │ │ │ ├── Background.colorset │ │ │ │ │ └── Contents.json │ │ │ │ ├── Contents.json │ │ │ │ ├── SecondaryBackground.colorset │ │ │ │ │ └── Contents.json │ │ │ │ └── Text.colorset │ │ │ │ │ └── Contents.json │ │ │ ├── Contents.json │ │ │ └── Images │ │ │ │ ├── Contents.json │ │ │ │ ├── CoupleWithMoon.imageset │ │ │ │ ├── Contents.json │ │ │ │ ├── file1 (1).svg │ │ │ │ └── file1.svg │ │ │ │ ├── Drum.imageset │ │ │ │ ├── Contents.json │ │ │ │ ├── file1 (3).svg │ │ │ │ └── file1.svg │ │ │ │ ├── Flowers.imageset │ │ │ │ ├── Contents.json │ │ │ │ ├── file1 (4).svg │ │ │ │ └── file1.svg │ │ │ │ ├── IntroImageOne.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── image 79.png │ │ │ │ ├── IntroImageTwo.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── image 78.png │ │ │ │ ├── LadyWithBag.imageset │ │ │ │ ├── Contents.json │ │ │ │ ├── file0.svg │ │ │ │ └── file1.svg │ │ │ │ ├── PromotionImageOne.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── image 80.png │ │ │ │ ├── PromotionImageTwo.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── image 80-1.png │ │ │ │ └── StrangeEyes.imageset │ │ │ │ ├── Contents.json │ │ │ │ ├── file1 (2).svg │ │ │ │ └── file1.svg │ │ ├── Fonts │ │ │ ├── Inter-Regular.ttf │ │ │ ├── Inter-SemiBold.ttf │ │ │ ├── Manrope-Bold.ttf │ │ │ ├── Manrope-ExtraBold.ttf │ │ │ ├── Manrope-Regular.ttf │ │ │ └── Manrope-SemiBold.ttf │ │ └── UIImage+MinimalOnboarding.swift │ │ ├── Screen.swift │ │ └── Utilities │ │ ├── Binding+SideEffect.swift │ │ ├── OnboardingAlert.swift │ │ └── View+Keyboard.swift │ └── Tests │ └── MinimalOnboardingTests │ └── MinimalOnboardingTests.swift └── readme.md /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unflowhq/OnboardingKit/a4358c615697dea86205bc0788bcd48419abc583/.DS_Store -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore 4 | & Swift.gitignore 5 | 6 | ## User settings 7 | xcuserdata/ 8 | 9 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 10 | *.xcscmblueprint 11 | *.xccheckout 12 | 13 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 14 | build/ 15 | DerivedData/ 16 | *.moved-aside 17 | *.pbxuser 18 | !default.pbxuser 19 | *.mode1v3 20 | !default.mode1v3 21 | *.mode2v3 22 | !default.mode2v3 23 | *.perspectivev3 24 | !default.perspectivev3 25 | 26 | ## Obj-C/Swift specific 27 | *.hmap 28 | 29 | ## App packaging 30 | *.ipa 31 | *.dSYM.zip 32 | *.dSYM 33 | 34 | ## Playgrounds 35 | timeline.xctimeline 36 | playground.xcworkspace 37 | 38 | # Swift Package Manager 39 | # 40 | # Add this line if you want to avoid checking in source code from Swift Package Manager 41 | dependencies. 42 | # Packages/ 43 | # Package.pins 44 | # Package.resolved 45 | # *.xcodeproj 46 | # 47 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 48 | # hence it is not needed unless you have added a package configuration file to your project 49 | # .swiftpm 50 | 51 | .build/ 52 | 53 | # CocoaPods 54 | # 55 | # We recommend against adding the Pods directory to your .gitignore. However 56 | # you should judge for yourself, the pros and cons are mentioned at: 57 | # 58 | https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 59 | # 60 | # Pods/ 61 | # 62 | # Add this line if you want to avoid checking in source code from the Xcode workspace 63 | # *.xcworkspace 64 | 65 | # Carthage 66 | # 67 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 68 | # Carthage/Checkouts 69 | 70 | Carthage/Build/ 71 | 72 | # Accio dependency management 73 | .accio/ 74 | 75 | # fastlane 76 | # 77 | # It is recommended to not store the screenshots in the git repo. 78 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 79 | # For more information about the recommended setup visit: 80 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 81 | 82 | fastlane/report.xml 83 | fastlane/Preview.html 84 | fastlane/screenshots/**/*.png 85 | fastlane/test_output 86 | 87 | # Code Injection 88 | # 89 | # After new code Injection tools there's a generated folder /iOSInjectionProject 90 | # https://github.com/johnno1962/injectionforxcode 91 | 92 | iOSInjectionProject/ 93 | 94 | -------------------------------------------------------------------------------- /Github/Cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unflowhq/OnboardingKit/a4358c615697dea86205bc0788bcd48419abc583/Github/Cover.png -------------------------------------------------------------------------------- /Github/MinimalOnboarding.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unflowhq/OnboardingKit/a4358c615697dea86205bc0788bcd48419abc583/Github/MinimalOnboarding.png -------------------------------------------------------------------------------- /OnboardingKit.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 56; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | FA00F27B2987CCB500EFAB8E /* OnboardingKitApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA00F27A2987CCB500EFAB8E /* OnboardingKitApp.swift */; }; 11 | FA00F27D2987CCB500EFAB8E /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA00F27C2987CCB500EFAB8E /* ContentView.swift */; }; 12 | FA00F27F2987CCB600EFAB8E /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = FA00F27E2987CCB600EFAB8E /* Assets.xcassets */; }; 13 | FA00F2822987CCB600EFAB8E /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = FA00F2812987CCB600EFAB8E /* Preview Assets.xcassets */; }; 14 | FA70CD752987D6DF00CD90C9 /* MinimalOnboarding in Frameworks */ = {isa = PBXBuildFile; productRef = FA70CD742987D6DF00CD90C9 /* MinimalOnboarding */; }; 15 | /* End PBXBuildFile section */ 16 | 17 | /* Begin PBXFileReference section */ 18 | FA00F2772987CCB500EFAB8E /* OnboardingKit.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = OnboardingKit.app; sourceTree = BUILT_PRODUCTS_DIR; }; 19 | FA00F27A2987CCB500EFAB8E /* OnboardingKitApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingKitApp.swift; sourceTree = ""; }; 20 | FA00F27C2987CCB500EFAB8E /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 21 | FA00F27E2987CCB600EFAB8E /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 22 | FA00F2812987CCB600EFAB8E /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 23 | FA00F29A2987D2D700EFAB8E /* MinimalOnboarding */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = MinimalOnboarding; sourceTree = ""; }; 24 | FA3000E52989424100D49559 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; 25 | FA5EECCB29882288004D49FE /* DesignHelpKit */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = DesignHelpKit; sourceTree = ""; }; 26 | /* End PBXFileReference section */ 27 | 28 | /* Begin PBXFrameworksBuildPhase section */ 29 | FA00F2742987CCB500EFAB8E /* Frameworks */ = { 30 | isa = PBXFrameworksBuildPhase; 31 | buildActionMask = 2147483647; 32 | files = ( 33 | FA70CD752987D6DF00CD90C9 /* MinimalOnboarding in Frameworks */, 34 | ); 35 | runOnlyForDeploymentPostprocessing = 0; 36 | }; 37 | /* End PBXFrameworksBuildPhase section */ 38 | 39 | /* Begin PBXGroup section */ 40 | FA00F26E2987CCB500EFAB8E = { 41 | isa = PBXGroup; 42 | children = ( 43 | FA00F2992987D2B600EFAB8E /* Packages */, 44 | FA00F2792987CCB500EFAB8E /* OnboardingKit */, 45 | FA00F2782987CCB500EFAB8E /* Products */, 46 | FA70CD732987D6DF00CD90C9 /* Frameworks */, 47 | ); 48 | sourceTree = ""; 49 | }; 50 | FA00F2782987CCB500EFAB8E /* Products */ = { 51 | isa = PBXGroup; 52 | children = ( 53 | FA00F2772987CCB500EFAB8E /* OnboardingKit.app */, 54 | ); 55 | name = Products; 56 | sourceTree = ""; 57 | }; 58 | FA00F2792987CCB500EFAB8E /* OnboardingKit */ = { 59 | isa = PBXGroup; 60 | children = ( 61 | FA3000E52989424100D49559 /* Info.plist */, 62 | FA00F2882987CCC000EFAB8E /* Supporting Files */, 63 | FA00F27A2987CCB500EFAB8E /* OnboardingKitApp.swift */, 64 | FA00F27C2987CCB500EFAB8E /* ContentView.swift */, 65 | FA00F27E2987CCB600EFAB8E /* Assets.xcassets */, 66 | FA00F2802987CCB600EFAB8E /* Preview Content */, 67 | ); 68 | path = OnboardingKit; 69 | sourceTree = ""; 70 | }; 71 | FA00F2802987CCB600EFAB8E /* Preview Content */ = { 72 | isa = PBXGroup; 73 | children = ( 74 | FA00F2812987CCB600EFAB8E /* Preview Assets.xcassets */, 75 | ); 76 | path = "Preview Content"; 77 | sourceTree = ""; 78 | }; 79 | FA00F2882987CCC000EFAB8E /* Supporting Files */ = { 80 | isa = PBXGroup; 81 | children = ( 82 | ); 83 | path = "Supporting Files"; 84 | sourceTree = ""; 85 | }; 86 | FA00F2992987D2B600EFAB8E /* Packages */ = { 87 | isa = PBXGroup; 88 | children = ( 89 | FA5EECCB29882288004D49FE /* DesignHelpKit */, 90 | FA00F29A2987D2D700EFAB8E /* MinimalOnboarding */, 91 | ); 92 | path = Packages; 93 | sourceTree = ""; 94 | }; 95 | FA70CD732987D6DF00CD90C9 /* Frameworks */ = { 96 | isa = PBXGroup; 97 | children = ( 98 | ); 99 | name = Frameworks; 100 | sourceTree = ""; 101 | }; 102 | /* End PBXGroup section */ 103 | 104 | /* Begin PBXNativeTarget section */ 105 | FA00F2762987CCB500EFAB8E /* OnboardingKit */ = { 106 | isa = PBXNativeTarget; 107 | buildConfigurationList = FA00F2852987CCB600EFAB8E /* Build configuration list for PBXNativeTarget "OnboardingKit" */; 108 | buildPhases = ( 109 | FA00F2732987CCB500EFAB8E /* Sources */, 110 | FA00F2742987CCB500EFAB8E /* Frameworks */, 111 | FA00F2752987CCB500EFAB8E /* Resources */, 112 | ); 113 | buildRules = ( 114 | ); 115 | dependencies = ( 116 | ); 117 | name = OnboardingKit; 118 | packageProductDependencies = ( 119 | FA70CD742987D6DF00CD90C9 /* MinimalOnboarding */, 120 | ); 121 | productName = OnboardingKit; 122 | productReference = FA00F2772987CCB500EFAB8E /* OnboardingKit.app */; 123 | productType = "com.apple.product-type.application"; 124 | }; 125 | /* End PBXNativeTarget section */ 126 | 127 | /* Begin PBXProject section */ 128 | FA00F26F2987CCB500EFAB8E /* Project object */ = { 129 | isa = PBXProject; 130 | attributes = { 131 | BuildIndependentTargetsInParallel = 1; 132 | LastSwiftUpdateCheck = 1420; 133 | LastUpgradeCheck = 1420; 134 | TargetAttributes = { 135 | FA00F2762987CCB500EFAB8E = { 136 | CreatedOnToolsVersion = 14.2; 137 | }; 138 | }; 139 | }; 140 | buildConfigurationList = FA00F2722987CCB500EFAB8E /* Build configuration list for PBXProject "OnboardingKit" */; 141 | compatibilityVersion = "Xcode 14.0"; 142 | developmentRegion = en; 143 | hasScannedForEncodings = 0; 144 | knownRegions = ( 145 | en, 146 | Base, 147 | ); 148 | mainGroup = FA00F26E2987CCB500EFAB8E; 149 | productRefGroup = FA00F2782987CCB500EFAB8E /* Products */; 150 | projectDirPath = ""; 151 | projectRoot = ""; 152 | targets = ( 153 | FA00F2762987CCB500EFAB8E /* OnboardingKit */, 154 | ); 155 | }; 156 | /* End PBXProject section */ 157 | 158 | /* Begin PBXResourcesBuildPhase section */ 159 | FA00F2752987CCB500EFAB8E /* Resources */ = { 160 | isa = PBXResourcesBuildPhase; 161 | buildActionMask = 2147483647; 162 | files = ( 163 | FA00F2822987CCB600EFAB8E /* Preview Assets.xcassets in Resources */, 164 | FA00F27F2987CCB600EFAB8E /* Assets.xcassets in Resources */, 165 | ); 166 | runOnlyForDeploymentPostprocessing = 0; 167 | }; 168 | /* End PBXResourcesBuildPhase section */ 169 | 170 | /* Begin PBXSourcesBuildPhase section */ 171 | FA00F2732987CCB500EFAB8E /* Sources */ = { 172 | isa = PBXSourcesBuildPhase; 173 | buildActionMask = 2147483647; 174 | files = ( 175 | FA00F27D2987CCB500EFAB8E /* ContentView.swift in Sources */, 176 | FA00F27B2987CCB500EFAB8E /* OnboardingKitApp.swift in Sources */, 177 | ); 178 | runOnlyForDeploymentPostprocessing = 0; 179 | }; 180 | /* End PBXSourcesBuildPhase section */ 181 | 182 | /* Begin XCBuildConfiguration section */ 183 | FA00F2832987CCB600EFAB8E /* Debug */ = { 184 | isa = XCBuildConfiguration; 185 | buildSettings = { 186 | ALWAYS_SEARCH_USER_PATHS = NO; 187 | CLANG_ANALYZER_NONNULL = YES; 188 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 189 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 190 | CLANG_ENABLE_MODULES = YES; 191 | CLANG_ENABLE_OBJC_ARC = YES; 192 | CLANG_ENABLE_OBJC_WEAK = YES; 193 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 194 | CLANG_WARN_BOOL_CONVERSION = YES; 195 | CLANG_WARN_COMMA = YES; 196 | CLANG_WARN_CONSTANT_CONVERSION = YES; 197 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 198 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 199 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 200 | CLANG_WARN_EMPTY_BODY = YES; 201 | CLANG_WARN_ENUM_CONVERSION = YES; 202 | CLANG_WARN_INFINITE_RECURSION = YES; 203 | CLANG_WARN_INT_CONVERSION = YES; 204 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 205 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 206 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 207 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 208 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 209 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 210 | CLANG_WARN_STRICT_PROTOTYPES = YES; 211 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 212 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 213 | CLANG_WARN_UNREACHABLE_CODE = YES; 214 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 215 | COPY_PHASE_STRIP = NO; 216 | DEBUG_INFORMATION_FORMAT = dwarf; 217 | ENABLE_STRICT_OBJC_MSGSEND = YES; 218 | ENABLE_TESTABILITY = YES; 219 | GCC_C_LANGUAGE_STANDARD = gnu11; 220 | GCC_DYNAMIC_NO_PIC = NO; 221 | GCC_NO_COMMON_BLOCKS = YES; 222 | GCC_OPTIMIZATION_LEVEL = 0; 223 | GCC_PREPROCESSOR_DEFINITIONS = ( 224 | "DEBUG=1", 225 | "$(inherited)", 226 | ); 227 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 228 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 229 | GCC_WARN_UNDECLARED_SELECTOR = YES; 230 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 231 | GCC_WARN_UNUSED_FUNCTION = YES; 232 | GCC_WARN_UNUSED_VARIABLE = YES; 233 | IPHONEOS_DEPLOYMENT_TARGET = 16.0; 234 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 235 | MTL_FAST_MATH = YES; 236 | ONLY_ACTIVE_ARCH = YES; 237 | SDKROOT = iphoneos; 238 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 239 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 240 | }; 241 | name = Debug; 242 | }; 243 | FA00F2842987CCB600EFAB8E /* Release */ = { 244 | isa = XCBuildConfiguration; 245 | buildSettings = { 246 | ALWAYS_SEARCH_USER_PATHS = NO; 247 | CLANG_ANALYZER_NONNULL = YES; 248 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 249 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 250 | CLANG_ENABLE_MODULES = YES; 251 | CLANG_ENABLE_OBJC_ARC = YES; 252 | CLANG_ENABLE_OBJC_WEAK = YES; 253 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 254 | CLANG_WARN_BOOL_CONVERSION = YES; 255 | CLANG_WARN_COMMA = YES; 256 | CLANG_WARN_CONSTANT_CONVERSION = YES; 257 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 258 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 259 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 260 | CLANG_WARN_EMPTY_BODY = YES; 261 | CLANG_WARN_ENUM_CONVERSION = YES; 262 | CLANG_WARN_INFINITE_RECURSION = YES; 263 | CLANG_WARN_INT_CONVERSION = YES; 264 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 265 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 266 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 267 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 268 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 269 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 270 | CLANG_WARN_STRICT_PROTOTYPES = YES; 271 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 272 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 273 | CLANG_WARN_UNREACHABLE_CODE = YES; 274 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 275 | COPY_PHASE_STRIP = NO; 276 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 277 | ENABLE_NS_ASSERTIONS = NO; 278 | ENABLE_STRICT_OBJC_MSGSEND = YES; 279 | GCC_C_LANGUAGE_STANDARD = gnu11; 280 | GCC_NO_COMMON_BLOCKS = YES; 281 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 282 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 283 | GCC_WARN_UNDECLARED_SELECTOR = YES; 284 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 285 | GCC_WARN_UNUSED_FUNCTION = YES; 286 | GCC_WARN_UNUSED_VARIABLE = YES; 287 | IPHONEOS_DEPLOYMENT_TARGET = 16.0; 288 | MTL_ENABLE_DEBUG_INFO = NO; 289 | MTL_FAST_MATH = YES; 290 | SDKROOT = iphoneos; 291 | SWIFT_COMPILATION_MODE = wholemodule; 292 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 293 | VALIDATE_PRODUCT = YES; 294 | }; 295 | name = Release; 296 | }; 297 | FA00F2862987CCB600EFAB8E /* Debug */ = { 298 | isa = XCBuildConfiguration; 299 | buildSettings = { 300 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 301 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 302 | CODE_SIGN_STYLE = Automatic; 303 | CURRENT_PROJECT_VERSION = 1; 304 | DEVELOPMENT_ASSET_PATHS = "\"OnboardingKit/Preview Content\""; 305 | DEVELOPMENT_TEAM = V636FA4NFP; 306 | ENABLE_PREVIEWS = YES; 307 | GENERATE_INFOPLIST_FILE = YES; 308 | INFOPLIST_FILE = OnboardingKit/Info.plist; 309 | INFOPLIST_KEY_LSApplicationCategoryType = ""; 310 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 311 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 312 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 313 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 314 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 315 | IPHONEOS_DEPLOYMENT_TARGET = 16.0; 316 | LD_RUNPATH_SEARCH_PATHS = ( 317 | "$(inherited)", 318 | "@executable_path/Frameworks", 319 | ); 320 | MARKETING_VERSION = 1.0; 321 | PRODUCT_BUNDLE_IDENTIFIER = com.unflow.OnboardingKit; 322 | PRODUCT_NAME = "$(TARGET_NAME)"; 323 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; 324 | SUPPORTS_MACCATALYST = NO; 325 | SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; 326 | SWIFT_EMIT_LOC_STRINGS = YES; 327 | SWIFT_VERSION = 5.0; 328 | TARGETED_DEVICE_FAMILY = 1; 329 | }; 330 | name = Debug; 331 | }; 332 | FA00F2872987CCB600EFAB8E /* Release */ = { 333 | isa = XCBuildConfiguration; 334 | buildSettings = { 335 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 336 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 337 | CODE_SIGN_STYLE = Automatic; 338 | CURRENT_PROJECT_VERSION = 1; 339 | DEVELOPMENT_ASSET_PATHS = "\"OnboardingKit/Preview Content\""; 340 | DEVELOPMENT_TEAM = V636FA4NFP; 341 | ENABLE_PREVIEWS = YES; 342 | GENERATE_INFOPLIST_FILE = YES; 343 | INFOPLIST_FILE = OnboardingKit/Info.plist; 344 | INFOPLIST_KEY_LSApplicationCategoryType = ""; 345 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 346 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 347 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 348 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 349 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 350 | IPHONEOS_DEPLOYMENT_TARGET = 16.0; 351 | LD_RUNPATH_SEARCH_PATHS = ( 352 | "$(inherited)", 353 | "@executable_path/Frameworks", 354 | ); 355 | MARKETING_VERSION = 1.0; 356 | PRODUCT_BUNDLE_IDENTIFIER = com.unflow.OnboardingKit; 357 | PRODUCT_NAME = "$(TARGET_NAME)"; 358 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; 359 | SUPPORTS_MACCATALYST = NO; 360 | SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; 361 | SWIFT_EMIT_LOC_STRINGS = YES; 362 | SWIFT_VERSION = 5.0; 363 | TARGETED_DEVICE_FAMILY = 1; 364 | }; 365 | name = Release; 366 | }; 367 | /* End XCBuildConfiguration section */ 368 | 369 | /* Begin XCConfigurationList section */ 370 | FA00F2722987CCB500EFAB8E /* Build configuration list for PBXProject "OnboardingKit" */ = { 371 | isa = XCConfigurationList; 372 | buildConfigurations = ( 373 | FA00F2832987CCB600EFAB8E /* Debug */, 374 | FA00F2842987CCB600EFAB8E /* Release */, 375 | ); 376 | defaultConfigurationIsVisible = 0; 377 | defaultConfigurationName = Release; 378 | }; 379 | FA00F2852987CCB600EFAB8E /* Build configuration list for PBXNativeTarget "OnboardingKit" */ = { 380 | isa = XCConfigurationList; 381 | buildConfigurations = ( 382 | FA00F2862987CCB600EFAB8E /* Debug */, 383 | FA00F2872987CCB600EFAB8E /* Release */, 384 | ); 385 | defaultConfigurationIsVisible = 0; 386 | defaultConfigurationName = Release; 387 | }; 388 | /* End XCConfigurationList section */ 389 | 390 | /* Begin XCSwiftPackageProductDependency section */ 391 | FA70CD742987D6DF00CD90C9 /* MinimalOnboarding */ = { 392 | isa = XCSwiftPackageProductDependency; 393 | productName = MinimalOnboarding; 394 | }; 395 | /* End XCSwiftPackageProductDependency section */ 396 | }; 397 | rootObject = FA00F26F2987CCB500EFAB8E /* Project object */; 398 | } 399 | -------------------------------------------------------------------------------- /OnboardingKit.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /OnboardingKit.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /OnboardingKit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "swift-case-paths", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/pointfreeco/swift-case-paths", 7 | "state" : { 8 | "revision" : "c3a42e8d1a76ff557cf565ed6d8b0aee0e6e75af", 9 | "version" : "0.11.0" 10 | } 11 | }, 12 | { 13 | "identity" : "swift-custom-dump", 14 | "kind" : "remoteSourceControl", 15 | "location" : "https://github.com/pointfreeco/swift-custom-dump", 16 | "state" : { 17 | "revision" : "87dd388a193569b288d03cb4060db54f90d1a66f", 18 | "version" : "0.7.0" 19 | } 20 | }, 21 | { 22 | "identity" : "swiftui-navigation", 23 | "kind" : "remoteSourceControl", 24 | "location" : "https://github.com/pointfreeco/swiftui-navigation", 25 | "state" : { 26 | "revision" : "270a754308f5440be52fc295242eb7031638bd15", 27 | "version" : "0.6.1" 28 | } 29 | }, 30 | { 31 | "identity" : "xctest-dynamic-overlay", 32 | "kind" : "remoteSourceControl", 33 | "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", 34 | "state" : { 35 | "revision" : "16b23a295fa322eb957af98037f86791449de60f", 36 | "version" : "0.8.1" 37 | } 38 | } 39 | ], 40 | "version" : 2 41 | } 42 | -------------------------------------------------------------------------------- /OnboardingKit.xcodeproj/xcshareddata/xcschemes/OnboardingKit.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /OnboardingKit/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unflowhq/OnboardingKit/a4358c615697dea86205bc0788bcd48419abc583/OnboardingKit/.DS_Store -------------------------------------------------------------------------------- /OnboardingKit/Assets.xcassets/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unflowhq/OnboardingKit/a4358c615697dea86205bc0788bcd48419abc583/OnboardingKit/Assets.xcassets/.DS_Store -------------------------------------------------------------------------------- /OnboardingKit/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "platform" : "universal", 6 | "reference" : "systemIndigoColor" 7 | }, 8 | "idiom" : "universal" 9 | } 10 | ], 11 | "info" : { 12 | "author" : "xcode", 13 | "version" : 1 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /OnboardingKit/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "icon_40.png", 5 | "idiom" : "iphone", 6 | "scale" : "2x", 7 | "size" : "20x20" 8 | }, 9 | { 10 | "filename" : "icon_60.png", 11 | "idiom" : "iphone", 12 | "scale" : "3x", 13 | "size" : "20x20" 14 | }, 15 | { 16 | "filename" : "icon_58.png", 17 | "idiom" : "iphone", 18 | "scale" : "2x", 19 | "size" : "29x29" 20 | }, 21 | { 22 | "filename" : "icon_87.png", 23 | "idiom" : "iphone", 24 | "scale" : "3x", 25 | "size" : "29x29" 26 | }, 27 | { 28 | "filename" : "icon_80.png", 29 | "idiom" : "iphone", 30 | "scale" : "2x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "filename" : "icon_120.png", 35 | "idiom" : "iphone", 36 | "scale" : "3x", 37 | "size" : "40x40" 38 | }, 39 | { 40 | "filename" : "icon_120.png", 41 | "idiom" : "iphone", 42 | "scale" : "2x", 43 | "size" : "60x60" 44 | }, 45 | { 46 | "filename" : "icon_180.png", 47 | "idiom" : "iphone", 48 | "scale" : "3x", 49 | "size" : "60x60" 50 | }, 51 | { 52 | "filename" : "icon_20.png", 53 | "idiom" : "ipad", 54 | "scale" : "1x", 55 | "size" : "20x20" 56 | }, 57 | { 58 | "filename" : "icon_40.png", 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "20x20" 62 | }, 63 | { 64 | "filename" : "icon_29.png", 65 | "idiom" : "ipad", 66 | "scale" : "1x", 67 | "size" : "29x29" 68 | }, 69 | { 70 | "filename" : "icon_58.png", 71 | "idiom" : "ipad", 72 | "scale" : "2x", 73 | "size" : "29x29" 74 | }, 75 | { 76 | "filename" : "icon_40.png", 77 | "idiom" : "ipad", 78 | "scale" : "1x", 79 | "size" : "40x40" 80 | }, 81 | { 82 | "filename" : "icon_80.png", 83 | "idiom" : "ipad", 84 | "scale" : "2x", 85 | "size" : "40x40" 86 | }, 87 | { 88 | "filename" : "icon_76.png", 89 | "idiom" : "ipad", 90 | "scale" : "1x", 91 | "size" : "76x76" 92 | }, 93 | { 94 | "filename" : "icon_152.png", 95 | "idiom" : "ipad", 96 | "scale" : "2x", 97 | "size" : "76x76" 98 | }, 99 | { 100 | "filename" : "icon_167.png", 101 | "idiom" : "ipad", 102 | "scale" : "2x", 103 | "size" : "83.5x83.5" 104 | }, 105 | { 106 | "filename" : "icon_1024.png", 107 | "idiom" : "ios-marketing", 108 | "scale" : "1x", 109 | "size" : "1024x1024" 110 | } 111 | ], 112 | "info" : { 113 | "author" : "xcode", 114 | "version" : 1 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /OnboardingKit/Assets.xcassets/AppIcon.appiconset/icon_1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unflowhq/OnboardingKit/a4358c615697dea86205bc0788bcd48419abc583/OnboardingKit/Assets.xcassets/AppIcon.appiconset/icon_1024.png -------------------------------------------------------------------------------- /OnboardingKit/Assets.xcassets/AppIcon.appiconset/icon_120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unflowhq/OnboardingKit/a4358c615697dea86205bc0788bcd48419abc583/OnboardingKit/Assets.xcassets/AppIcon.appiconset/icon_120.png -------------------------------------------------------------------------------- /OnboardingKit/Assets.xcassets/AppIcon.appiconset/icon_152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unflowhq/OnboardingKit/a4358c615697dea86205bc0788bcd48419abc583/OnboardingKit/Assets.xcassets/AppIcon.appiconset/icon_152.png -------------------------------------------------------------------------------- /OnboardingKit/Assets.xcassets/AppIcon.appiconset/icon_167.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unflowhq/OnboardingKit/a4358c615697dea86205bc0788bcd48419abc583/OnboardingKit/Assets.xcassets/AppIcon.appiconset/icon_167.png -------------------------------------------------------------------------------- /OnboardingKit/Assets.xcassets/AppIcon.appiconset/icon_180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unflowhq/OnboardingKit/a4358c615697dea86205bc0788bcd48419abc583/OnboardingKit/Assets.xcassets/AppIcon.appiconset/icon_180.png -------------------------------------------------------------------------------- /OnboardingKit/Assets.xcassets/AppIcon.appiconset/icon_20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unflowhq/OnboardingKit/a4358c615697dea86205bc0788bcd48419abc583/OnboardingKit/Assets.xcassets/AppIcon.appiconset/icon_20.png -------------------------------------------------------------------------------- /OnboardingKit/Assets.xcassets/AppIcon.appiconset/icon_29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unflowhq/OnboardingKit/a4358c615697dea86205bc0788bcd48419abc583/OnboardingKit/Assets.xcassets/AppIcon.appiconset/icon_29.png -------------------------------------------------------------------------------- /OnboardingKit/Assets.xcassets/AppIcon.appiconset/icon_40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unflowhq/OnboardingKit/a4358c615697dea86205bc0788bcd48419abc583/OnboardingKit/Assets.xcassets/AppIcon.appiconset/icon_40.png -------------------------------------------------------------------------------- /OnboardingKit/Assets.xcassets/AppIcon.appiconset/icon_58.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unflowhq/OnboardingKit/a4358c615697dea86205bc0788bcd48419abc583/OnboardingKit/Assets.xcassets/AppIcon.appiconset/icon_58.png -------------------------------------------------------------------------------- /OnboardingKit/Assets.xcassets/AppIcon.appiconset/icon_60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unflowhq/OnboardingKit/a4358c615697dea86205bc0788bcd48419abc583/OnboardingKit/Assets.xcassets/AppIcon.appiconset/icon_60.png -------------------------------------------------------------------------------- /OnboardingKit/Assets.xcassets/AppIcon.appiconset/icon_76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unflowhq/OnboardingKit/a4358c615697dea86205bc0788bcd48419abc583/OnboardingKit/Assets.xcassets/AppIcon.appiconset/icon_76.png -------------------------------------------------------------------------------- /OnboardingKit/Assets.xcassets/AppIcon.appiconset/icon_80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unflowhq/OnboardingKit/a4358c615697dea86205bc0788bcd48419abc583/OnboardingKit/Assets.xcassets/AppIcon.appiconset/icon_80.png -------------------------------------------------------------------------------- /OnboardingKit/Assets.xcassets/AppIcon.appiconset/icon_87.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unflowhq/OnboardingKit/a4358c615697dea86205bc0788bcd48419abc583/OnboardingKit/Assets.xcassets/AppIcon.appiconset/icon_87.png -------------------------------------------------------------------------------- /OnboardingKit/Assets.xcassets/BackgroundGrey.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.965", 9 | "green" : "0.965", 10 | "red" : "0.965" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0.098", 27 | "green" : "0.098", 28 | "red" : "0.098" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /OnboardingKit/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /OnboardingKit/Assets.xcassets/DynamicWhite.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "display-p3", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "1.000", 9 | "green" : "0.992", 10 | "red" : "1.000" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "display-p3", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0.200", 27 | "green" : "0.200", 28 | "red" : "0.200" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /OnboardingKit/Assets.xcassets/Header.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Header.pdf", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "preserves-vector-representation" : true, 14 | "template-rendering-intent" : "original" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /OnboardingKit/Assets.xcassets/Header.imageset/Header.pdf: -------------------------------------------------------------------------------- 1 | %PDF-1.7 2 | 3 | 1 0 obj 4 | << /Length 2 0 R 5 | /Range [ 0.000000 1.000000 0.000000 1.000000 0.000000 1.000000 ] 6 | /Domain [ 0.000000 1.000000 ] 7 | /FunctionType 4 8 | >> 9 | stream 10 | { 0.111083 exch 0.000000 exch 0.179167 exch dup 0.000000 gt { exch pop exch pop exch pop dup 0.000000 sub -0.042833 mul 0.111083 add exch dup 0.000000 sub 0.000000 mul 0.000000 add exch dup 0.000000 sub 0.083333 mul 0.179167 add exch } if dup 1.000000 gt { exch pop exch pop exch pop 0.068250 exch 0.000000 exch 0.262500 exch } if pop } 11 | endstream 12 | endobj 13 | 14 | 2 0 obj 15 | 337 16 | endobj 17 | 18 | 3 0 obj 19 | << /Pattern << /P1 << /Matrix [ 0.000000 -200.000000 200.000000 0.000000 -200.000000 200.000000 ] 20 | /Shading << /Coords [ 0.000000 0.000000 1.000000 0.000000 ] 21 | /ColorSpace /DeviceRGB 22 | /Function 1 0 R 23 | /Domain [ 0.000000 1.000000 ] 24 | /ShadingType 2 25 | /Extend [ true true ] 26 | >> 27 | /PatternType 2 28 | /Type /Pattern 29 | >> >> >> 30 | endobj 31 | 32 | 4 0 obj 33 | << /Length 5 0 R >> 34 | stream 35 | /DeviceRGB CS 36 | /DeviceRGB cs 37 | q 38 | 1.000000 0.000000 -0.000000 1.000000 0.000000 0.000000 cm 39 | /Pattern cs 40 | /P1 scn 41 | 0.000000 200.000000 m 42 | 377.000000 200.000000 l 43 | 377.000000 0.000000 l 44 | 0.000000 0.000000 l 45 | 0.000000 200.000000 l 46 | h 47 | f 48 | n 49 | Q 50 | q 51 | 1.000000 0.000000 -0.000000 1.000000 107.000000 77.495605 cm 52 | 1.000000 1.000000 1.000000 scn 53 | 22.252199 38.941345 m 54 | 22.252199 42.013733 19.761538 44.504395 16.689150 44.504395 c 55 | 5.563050 44.504395 l 56 | 2.490665 44.504395 -0.000000 42.013733 0.000000 38.941345 c 57 | 0.000000 5.563049 l 58 | 0.000000 2.490662 2.490665 0.000000 5.563050 0.000000 c 59 | 16.689150 0.000000 l 60 | 19.761538 0.000000 22.252199 2.490662 22.252199 5.563049 c 61 | 22.252199 8.345345 l 62 | 28.621941 8.345779 34.277119 11.407681 37.824947 16.131008 c 63 | 38.516903 17.052223 38.331047 18.359955 37.409832 19.051908 c 64 | 36.488613 19.743860 35.180882 19.558006 34.488930 18.636791 c 65 | 31.695229 14.917458 27.254227 12.518064 22.252199 12.517630 c 66 | 22.252199 38.941345 l 67 | h 68 | 7.091807 19.051908 m 69 | 8.013023 19.743860 9.320757 19.558006 10.012708 18.636791 c 70 | 12.806661 14.917120 17.249617 12.517630 22.252199 12.517630 c 71 | 22.252199 8.345345 l 72 | 15.881904 8.345345 10.224768 11.407345 6.676688 16.131008 c 73 | 5.984734 17.052223 6.170590 18.359955 7.091807 19.051908 c 74 | h 75 | f* 76 | n 77 | Q 78 | q 79 | 1.000000 0.000000 -0.000000 1.000000 107.000000 113.655426 cm 80 | 1.000000 1.000000 1.000000 scn 81 | 26.424486 -6.953812 m 82 | 26.424486 -9.258102 28.292482 -11.126099 30.596773 -11.126099 c 83 | 32.901066 -11.126099 34.769062 -9.258102 34.769062 -6.953812 c 84 | 34.769062 -4.649521 32.901066 -2.781525 30.596773 -2.781525 c 85 | 28.292482 -2.781525 26.424486 -4.649521 26.424486 -6.953812 c 86 | h 87 | f 88 | n 89 | Q 90 | q 91 | 1.000000 0.000000 -0.000000 1.000000 107.000000 96.362686 cm 92 | 1.000000 1.000000 1.000000 scn 93 | 52.598637 -0.397758 m 94 | 52.562477 -5.821732 55.346786 -10.884102 62.759552 -10.884102 c 95 | 70.172318 -10.884102 72.956619 -5.821732 72.775818 -0.397758 c 96 | 72.775818 14.753210 l 97 | 68.436638 14.753210 l 98 | 68.436638 -0.397758 l 99 | 68.400482 -4.049900 66.954086 -6.942684 62.759552 -6.942684 c 100 | 58.565010 -6.942684 56.937817 -4.049900 56.937817 -0.397758 c 101 | 56.937817 14.753210 l 102 | 52.598637 14.753210 l 103 | 52.598637 -0.397758 l 104 | h 105 | f 106 | n 107 | Q 108 | q 109 | 1.000000 0.000000 -0.000000 1.000000 107.000000 103.522324 cm 110 | 1.000000 1.000000 1.000000 scn 111 | 76.074272 -17.718304 m 112 | 76.074272 0.397766 l 113 | 80.413452 0.397766 l 114 | 80.413452 -2.205740 l 115 | 81.353607 -0.470068 83.089279 0.759365 85.656631 0.759365 c 116 | 90.104286 0.759365 92.382355 -2.314220 92.382355 -6.798038 c 117 | 92.382355 -17.718304 l 118 | 88.043175 -17.718304 l 119 | 88.043175 -7.448912 l 120 | 88.043175 -4.736927 86.813736 -3.037416 84.354866 -3.037416 c 121 | 81.896004 -3.037416 80.413452 -4.736927 80.413452 -7.448912 c 122 | 80.413452 -17.718304 l 123 | 76.074272 -17.718304 l 124 | h 125 | f 126 | n 127 | Q 128 | q 129 | 1.000000 0.000000 -0.000000 1.000000 107.000000 96.290359 cm 130 | 1.000000 1.000000 1.000000 scn 131 | 94.028473 4.122229 m 132 | 96.776619 4.122229 l 133 | 96.776619 -10.486340 l 134 | 101.151962 -10.486340 l 135 | 101.151962 4.122229 l 136 | 104.876427 4.122229 l 137 | 104.876427 7.593571 l 138 | 101.151962 7.593571 l 139 | 101.151962 8.967644 l 140 | 101.151962 10.992596 102.164436 11.715791 103.755470 11.715791 c 141 | 104.153229 11.715791 104.478661 11.679632 104.804100 11.607311 c 142 | 104.804100 14.825537 l 143 | 104.153229 15.042495 103.285385 15.223294 102.309067 15.223294 c 144 | 98.837723 15.223294 96.776619 13.126023 96.776619 9.510042 c 145 | 96.776619 7.593571 l 146 | 93.992310 7.593571 l 147 | 94.028473 4.122229 l 148 | h 149 | f 150 | n 151 | Q 152 | q 153 | 1.000000 0.000000 -0.000000 1.000000 107.000000 94.482368 cm 154 | 1.000000 1.000000 1.000000 scn 155 | 107.231041 18.839275 m 156 | 107.231041 -8.678349 l 157 | 111.570236 -8.678349 l 158 | 111.570236 18.839275 l 159 | 107.231041 18.839275 l 160 | h 161 | f 162 | n 163 | Q 164 | q 165 | 1.000000 0.000000 -0.000000 1.000000 107.000000 103.233055 cm 166 | 1.000000 1.000000 1.000000 scn 167 | 123.277534 -17.754471 m 168 | 128.918472 -17.754471 132.642929 -13.451458 132.642929 -8.389082 c 169 | 132.642929 -3.326706 128.882309 1.012474 123.277534 1.012474 c 170 | 117.600441 1.012474 113.912140 -3.290546 113.912140 -8.389082 c 171 | 113.912140 -13.451458 117.600441 -17.754471 123.277534 -17.754471 c 172 | h 173 | 123.277534 -13.776897 m 174 | 120.601707 -13.776897 118.251320 -11.788105 118.251320 -8.389082 c 175 | 118.251320 -4.953899 120.601707 -2.965107 123.277534 -2.965107 c 176 | 125.917198 -2.965107 128.267593 -4.953899 128.267593 -8.389082 c 177 | 128.267593 -11.788105 125.917198 -13.776897 123.277534 -13.776897 c 178 | h 179 | f 180 | n 181 | Q 182 | q 183 | 1.000000 0.000000 -0.000000 1.000000 107.000000 103.920090 cm 184 | 1.000000 1.000000 1.000000 scn 185 | 132.819763 0.000000 m 186 | 138.605347 -18.079910 l 187 | 143.233810 -18.079910 l 188 | 147.283707 -5.604771 l 189 | 151.297455 -18.079910 l 190 | 155.925888 -18.079910 l 191 | 161.747635 0.000000 l 192 | 157.191498 0.000000 l 193 | 153.503204 -12.728258 l 194 | 149.561783 0.000000 l 195 | 145.005646 0.000000 l 196 | 141.064209 -12.728258 l 197 | 137.375900 0.000000 l 198 | 132.819763 0.000000 l 199 | h 200 | f 201 | n 202 | Q 203 | 204 | endstream 205 | endobj 206 | 207 | 5 0 obj 208 | 4621 209 | endobj 210 | 211 | 6 0 obj 212 | << /Annots [] 213 | /Type /Page 214 | /MediaBox [ 0.000000 0.000000 377.000000 200.000000 ] 215 | /Resources 3 0 R 216 | /Contents 4 0 R 217 | /Parent 7 0 R 218 | >> 219 | endobj 220 | 221 | 7 0 obj 222 | << /Kids [ 6 0 R ] 223 | /Count 1 224 | /Type /Pages 225 | >> 226 | endobj 227 | 228 | 8 0 obj 229 | << /Pages 7 0 R 230 | /Type /Catalog 231 | >> 232 | endobj 233 | 234 | xref 235 | 0 9 236 | 0000000000 65535 f 237 | 0000000010 00000 n 238 | 0000000531 00000 n 239 | 0000000553 00000 n 240 | 0000001183 00000 n 241 | 0000005860 00000 n 242 | 0000005883 00000 n 243 | 0000006058 00000 n 244 | 0000006132 00000 n 245 | trailer 246 | << /ID [ (some) (id) ] 247 | /Root 8 0 R 248 | /Size 9 249 | >> 250 | startxref 251 | 6191 252 | %%EOF -------------------------------------------------------------------------------- /OnboardingKit/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // OnboardingKit 4 | // 5 | // Created by Alex Logan on 30/01/2023. 6 | // 7 | 8 | import SwiftUI 9 | import MinimalOnboarding 10 | import SwiftUINavigation 11 | 12 | struct ContentView: View { 13 | @State private var flow: Flow? 14 | 15 | var listRowBackgroundColor: Color { 16 | Color("DynamicWhite") 17 | } 18 | 19 | var body: some View { 20 | NavigationStack { 21 | List { 22 | Image("Header") 23 | .resizable() 24 | .aspectRatio(contentMode: .fill) 25 | .listRowInsets(EdgeInsets()) 26 | 27 | Section(header: Text("Unflow")) { 28 | Text("OnboardingKit is brought to you by Unflow. Unflow is a next-generation mobile CMS that allows you to set gorgeous native screens live remotley with no need for developer intervention.") 29 | urlButton(url: "https://unflow.com/onboarding", text: "Check out Unflow") 30 | urlButton(url: "https://www.figma.com/community/file/1197544767192716804", text: "OnboardingKit on Figma") 31 | } 32 | .listRowBackground(listRowBackgroundColor) 33 | 34 | Section(header: Text("Demo")) { 35 | Text("Select a flow to preview the full onboarding experience.") 36 | Text("Flows are designed to emulate how they'd behave for real, so you can only exit at points where you'd typically exit.") 37 | } 38 | .listRowBackground(listRowBackgroundColor) 39 | 40 | Section(header: Text("Flows"), footer: Text("More flows are coming soon.")) { 41 | Button(action: { 42 | self.flow = .minimal 43 | }, label: { 44 | Text("Minimal Flow") 45 | }) 46 | } 47 | .listRowBackground(listRowBackgroundColor) 48 | 49 | Section(footer: Text("Made with ❤️ and lots of ☕️ by @SwiftyAlex").fontWeight(.bold)) { EmptyView() } 50 | } 51 | .listRowBackground(listRowBackgroundColor) 52 | .navigationTitle("OnboardingKit") 53 | .fullScreenCover(unwrapping: $flow) { flow in 54 | switch flow.wrappedValue { 55 | case .minimal: 56 | OnboardingFlow(flowModel: .init(handler: { _ in 57 | self.flow = nil 58 | })) 59 | } 60 | } 61 | .scrollContentBackground(.hidden) 62 | .background( 63 | Color("BackgroundGrey").edgesIgnoringSafeArea(.all) 64 | ) 65 | } 66 | } 67 | 68 | private func urlButton(url: String, text: String) -> some View { 69 | Button(action: { 70 | guard let url = URL(string: url) else { return } 71 | UIApplication.shared.open(url) 72 | }, label: { 73 | Text(text) 74 | }) 75 | } 76 | } 77 | 78 | private enum Flow { 79 | case minimal 80 | } 81 | 82 | struct ContentView_Previews: PreviewProvider { 83 | static var previews: some View { 84 | ContentView() 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /OnboardingKit/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleURLTypes 6 | 7 | 8 | CFBundleTypeRole 9 | Viewer 10 | CFBundleURLName 11 | com.unflow.OnboardingKit.minimal 12 | CFBundleURLSchemes 13 | 14 | minimal-onboarding 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /OnboardingKit/OnboardingKitApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OnboardingKitApp.swift 3 | // OnboardingKit 4 | // 5 | // Created by Alex Logan on 30/01/2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @main 11 | struct OnboardingKitApp: App { 12 | var body: some Scene { 13 | WindowGroup { 14 | ContentView() 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /OnboardingKit/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /OnboardingKit/Supporting Files/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unflowhq/OnboardingKit/a4358c615697dea86205bc0788bcd48419abc583/OnboardingKit/Supporting Files/.DS_Store -------------------------------------------------------------------------------- /Packages/DesignHelpKit/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/config/registries.json 8 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 9 | .netrc 10 | -------------------------------------------------------------------------------- /Packages/DesignHelpKit/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.7 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "DesignHelpKit", 8 | platforms: [ 9 | .iOS(.v14) 10 | ], 11 | products: [ 12 | // Products define the executables and libraries a package produces, and make them visible to other packages. 13 | .library( 14 | name: "DesignHelpKit", 15 | targets: ["DesignHelpKit"]) 16 | ], 17 | dependencies: [ 18 | // Dependencies declare other packages that this package depends on. 19 | // .package(url: /* package url */, from: "1.0.0"), 20 | ], 21 | targets: [ 22 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 23 | // Targets can depend on other targets in this package, and on products in packages this package depends on. 24 | .target( 25 | name: "DesignHelpKit", 26 | dependencies: []), 27 | .testTarget( 28 | name: "DesignHelpKitTests", 29 | dependencies: ["DesignHelpKit"]) 30 | ] 31 | ) 32 | -------------------------------------------------------------------------------- /Packages/DesignHelpKit/README.md: -------------------------------------------------------------------------------- 1 | # DesignHelpKit 2 | 3 | A micro package to help when working with design assets in other packages. 4 | 5 | It provides helpers that dont have any underlying knowledge of the assets they work with - you'll have to provide that. 6 | 7 | ## Fonts 8 | 9 | `FontDisplayable` is a simple wrapper to allow modelling fonts without needing direct knowledge of the font. 10 | There's neat extensions included to handle registering your fonts too - without needing to use Info.plist's. 11 | 12 | ## Sample 13 | 14 | For example of the usage of this package, see `MinimalOnboarding`. 15 | 16 | It uses `DesignHelpKit` to make sure that its fonts are registered, and that its images can nicely come from the module without too much work. 17 | This reduces the amount of work needed for clean callsites to just one extension to provide the `.module` as a default argument. 18 | 19 | Fonts should be processed in your `Package.swift` in order to be compatible with `DesignHelpKit`. 20 | -------------------------------------------------------------------------------- /Packages/DesignHelpKit/Sources/DesignHelpKit/FontDisplayable.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | public protocol FontDisplayable { 4 | var name: String { get } 5 | 6 | func font(size: CGFloat, relativeTo: Font.TextStyle) -> Font 7 | } 8 | 9 | public extension FontDisplayable { 10 | func font(size: CGFloat, relativeTo style: Font.TextStyle) -> Font { 11 | Font.custom(name, size: size, relativeTo: style) 12 | } 13 | } 14 | 15 | public extension UIFont { 16 | static func register(from url: URL) { 17 | guard let fontDataProvider = CGDataProvider(url: url as CFURL) else { return } 18 | guard let font = CGFont(fontDataProvider) else { return } 19 | var error: Unmanaged? 20 | guard CTFontManagerRegisterGraphicsFont(font, &error) else { 21 | print("Error registering font from \(url): \(error.debugDescription)") 22 | return 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Packages/DesignHelpKit/Sources/DesignHelpKit/UIImage+Module.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIImage+Module.swift 3 | // 4 | // 5 | // Created by Alex Logan on 30/01/2023. 6 | // 7 | 8 | import Foundation 9 | import UIKit 10 | 11 | /// Convenience wrapper to get an image from the current package and displaying a warning triangle when it fails 12 | public extension UIImage { 13 | static func fromPackage(named name: String, bundle: Bundle) -> UIImage { 14 | return .init(packageImageName: name, bundle: bundle) ?? UIImage(systemName: "exclamationmark.triangle.fill") ?? UIImage() 15 | } 16 | 17 | convenience init?(packageImageName: String, bundle: Bundle) { 18 | self.init(named: packageImageName, in: bundle, with: nil) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Packages/DesignHelpKit/Tests/DesignHelpKitTests/DesignHelpKitTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import DesignHelpKit 3 | 4 | final class DesignHelpKitTests: XCTestCase { 5 | func testExample() throws { } 6 | } 7 | -------------------------------------------------------------------------------- /Packages/MinimalOnboarding/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/config/registries.json 8 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 9 | .netrc 10 | -------------------------------------------------------------------------------- /Packages/MinimalOnboarding/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.7 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "MinimalOnboarding", 8 | platforms: [ 9 | .iOS(.v16) 10 | ], 11 | products: [ 12 | .library( 13 | name: "MinimalOnboarding", 14 | targets: ["MinimalOnboarding"] 15 | ) 16 | ], 17 | dependencies: [ 18 | .package(name: "DesignHelpKit", path: "../DesignHelpKit"), 19 | .package(url: "https://github.com/pointfreeco/swiftui-navigation", from: "0.6.1") 20 | ], 21 | targets: [ 22 | .target( 23 | name: "MinimalOnboarding", 24 | dependencies: [ 25 | "DesignHelpKit", 26 | .product(name: "SwiftUINavigation", package: "swiftui-navigation") 27 | ], 28 | resources: [ 29 | .process("Resources/Fonts"), 30 | .process("Resources/Assets.xcassets") 31 | ] 32 | ), 33 | .testTarget( 34 | name: "MinimalOnboardingTests", 35 | dependencies: ["MinimalOnboarding"]) 36 | ] 37 | ) 38 | -------------------------------------------------------------------------------- /Packages/MinimalOnboarding/README.md: -------------------------------------------------------------------------------- 1 | # MinimalOnboarding 2 | 3 | ![MinimalOnboarding](/Github/MinimalOnboarding.png) 4 | 5 | Minimal onboarding is based on our clean template from [OnboardingKit](https://www.figma.com/community/file/1197544767192716804). 6 | 7 | There's two flows, login and signup. When you login, you're just prompted to add your phone number, then give notification permissions. 8 | 9 | For signup, you're asked to provide email first ( and verify it ), then you get some extra promotional materials with placeholder text. 10 | 11 | ## Architecture 12 | 13 | The architecture is a simple redux-ish design, with some exceptions to help with portability of these screens. There's one model `OnboardingFlowModel` that maintains state and the navigation path. 14 | 15 | All pages pass back their actions to the model, which in turn updates its own state and returns a result. 16 | 17 | The types for each pages actions and results are unique, and could be flattened if you choose to use a TCA style design. 18 | 19 | Each page gets provided initial state from the model, but generally hosts this initial state inside a `State` modifier. 20 | This is to aid in portability if you choose to not use this architecture here and just lift individual screens. 21 | 22 | ## How to use 23 | 24 | You'll need the links to be setup in your info.plist for email validation to work. Specifically, this uses "minimal-onboarding://email-verified?email=email@email.com" as the verification technique. 25 | 26 | If you're looking to test this, you can use the terminal command `xcrun simctl openurl booted 'minimal-onboarding://email-verification?email=email@email.com'. 27 | 28 | Throughout the package are small `// Task:` markers. These highlight any placeholders where you should implement real functionality instead of placeholders. 29 | 30 | 31 | ## Acknowledgements 32 | 33 | The drawings are from the Nankin pack on [storytale](https://storytale.io/pack/348). If you want to use these templates, you'll need to make sure you have access to them. 34 | 35 | Inside the app, we use some of the alert functionality from [SwiftUI-Navigation](https://github.com/pointfreeco/swiftui-navigation) by [PointFree](https://www.pointfree.co). 36 | -------------------------------------------------------------------------------- /Packages/MinimalOnboarding/Sources/MinimalOnboarding/Dependencies/AuthService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AuthService.swift 3 | // 4 | // 5 | // Created by Alex Logan on 30/01/2023. 6 | // 7 | 8 | import Foundation 9 | 10 | public protocol AuthService { 11 | func sendValidationEmail(to email: String) async -> Bool 12 | func sendValidationCode(to phoneNumber: String) async -> String? 13 | } 14 | 15 | public struct PlaceholderAuthService: AuthService { 16 | public init() { } 17 | 18 | @discardableResult 19 | public func sendValidationEmail(to email: String) async -> Bool { 20 | // Task: Add an email service into this logic instead of just sleeping and then firing off a hard coded success/fail. 21 | try? await Task.sleep(nanoseconds: UInt64(2_000_000_000)) 22 | print("AuthService: Email Sent!") 23 | // False indicates an error 24 | return true 25 | } 26 | 27 | @discardableResult 28 | public func sendValidationCode(to phoneNumber: String) async -> String? { 29 | // Task: Add a code service into this logic instead of just sleeping and then firing off a hard coded success/fail. 30 | try? await Task.sleep(nanoseconds: UInt64(2_000_000_000)) 31 | print("AuthService: Phone Code Sent!") 32 | // Nil indicates an error 33 | return "198913" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Packages/MinimalOnboarding/Sources/MinimalOnboarding/Dependencies/ContactValidator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ValidationService.swift 3 | // 4 | // 5 | // Created by Alex Logan on 30/01/2023. 6 | // 7 | 8 | import Foundation 9 | 10 | public protocol ContactValidating { 11 | func validateEmail(string: String) -> Bool 12 | func validatePhoneNumber(string: String) -> Bool 13 | } 14 | 15 | public struct ContactValidator: ContactValidating { 16 | public init() { } 17 | 18 | public func validateEmail(string: String) -> Bool { 19 | guard !string.isEmpty else { return false } 20 | let types: NSTextCheckingResult.CheckingType = [.link] 21 | guard let detector = try? NSDataDetector(types: types.rawValue) else { 22 | return false 23 | } 24 | let range = fullRange(for: string) 25 | let matches = detector.matches(in: string, options: [], range: range) 26 | guard matches.count == 1 else { 27 | return false 28 | } 29 | guard let result = matches.first, result.resultType == .link else { return false } 30 | guard NSEqualRanges(result.range, range) else { return false } 31 | return true 32 | } 33 | 34 | /// This is a very rudimentary check for a phone number, and will need to be extended in the real world. 35 | public func validatePhoneNumber(string: String) -> Bool { 36 | do { 37 | let regexString = #"^[\+]?[(]?[0-9]{3}[)]?[-\s\.]?[0-9]{3}[-\s\.]?[0-9]{4,6}$"# 38 | let regex = try Regex(regexString) 39 | guard let match = string.firstMatch(of: regex) else { return false } 40 | return match.first != nil 41 | } catch { 42 | print("Unable to validate phone numbers at this time due to \(error.localizedDescription)") 43 | dump(error) 44 | return false 45 | } 46 | } 47 | } 48 | 49 | private extension ContactValidator { 50 | func fullRange(for string: String) -> NSRange { 51 | NSRange(location: 0, length: string.count) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Packages/MinimalOnboarding/Sources/MinimalOnboarding/Dependencies/LinkParser.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LinkParser.swift 3 | // 4 | // 5 | // Created by Alex Logan on 31/01/2023. 6 | // 7 | 8 | import Foundation 9 | 10 | public protocol LinkParsing { 11 | func validate(url: URL) -> Link? 12 | } 13 | 14 | public struct LinkParser: LinkParsing { 15 | public init() { } 16 | 17 | public func validate(url: URL) -> Link? { 18 | guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return nil } 19 | guard components.scheme == "minimal-onboarding" else { return nil } 20 | 21 | switch components.host { 22 | case "email-verification": 23 | return parseEmaiVerificationLink(url: url, components: components) 24 | default: 25 | return nil 26 | } 27 | } 28 | } 29 | 30 | private extension LinkParser { 31 | func parseEmaiVerificationLink(url: URL, components: URLComponents) -> Link? { 32 | guard let email = components.queryItems?.first(where: { $0.name == "email" })?.value else { return nil } 33 | return .emailVerification(email: email) 34 | } 35 | } 36 | 37 | public enum Link { 38 | case emailVerification(email: String) 39 | } 40 | -------------------------------------------------------------------------------- /Packages/MinimalOnboarding/Sources/MinimalOnboarding/Dependencies/NotificationPermissionProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NotificationPermissionProviding.swift 3 | // 4 | // 5 | // Created by Alex Logan on 31/01/2023. 6 | // 7 | 8 | import Foundation 9 | import UserNotifications 10 | 11 | public protocol NotificationPermissionProviding { 12 | func requestPermission() async -> NotificationPermissionProvider.NotificationResult 13 | } 14 | 15 | public protocol NotificationPermissionGranter { 16 | func requestAuthorization() async throws -> Bool 17 | } 18 | 19 | extension UNUserNotificationCenter: NotificationPermissionGranter { 20 | public func requestAuthorization() async throws -> Bool { 21 | return try await self.requestAuthorization(options: [.alert, .badge, .sound]) 22 | } 23 | } 24 | 25 | public struct NotificationPermissionProvider: NotificationPermissionProviding { 26 | private let notificationCentre: NotificationPermissionGranter 27 | 28 | public init( 29 | notificationCentre: NotificationPermissionGranter = UNUserNotificationCenter.current() 30 | ) { 31 | self.notificationCentre = notificationCentre 32 | } 33 | 34 | @MainActor 35 | @discardableResult 36 | public func requestPermission() async -> NotificationResult { 37 | do { 38 | _ = try await notificationCentre.requestAuthorization() 39 | return .success 40 | } catch { 41 | return .failure(error) 42 | } 43 | } 44 | 45 | public enum NotificationResult { 46 | case success, failure(Error) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Packages/MinimalOnboarding/Sources/MinimalOnboarding/Dependencies/UserService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserService.swift 3 | // 4 | // 5 | // Created by Alex Logan on 30/01/2023. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | public protocol UserService { 12 | func store(email: String?) 13 | func store(phoneNumber: String?) 14 | func clear() 15 | } 16 | 17 | public struct AppStorageUserService: UserService { 18 | // Task: These should ideally be in the keychain 19 | @AppStorage("login_email") private var email: String? 20 | @AppStorage("login_phone") private var phoneNumber: String? 21 | 22 | public init() { } 23 | 24 | public func store(email: String?) { 25 | self.email = email 26 | } 27 | 28 | public func store(phoneNumber: String?) { 29 | self.phoneNumber = phoneNumber 30 | } 31 | 32 | public func clear() { 33 | self.email = nil 34 | self.phoneNumber = nil 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Packages/MinimalOnboarding/Sources/MinimalOnboarding/Design/Color.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Color.swift 3 | // 4 | // 5 | // Created by Alex Logan on 30/01/2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | extension Color { 11 | static var text: Color { 12 | Color(UIColor(named: "Text", in: .module, compatibleWith: nil) ?? .label) 13 | } 14 | 15 | static var primaryBackground: Color { 16 | Color(UIColor(named: "Background", in: .module, compatibleWith: nil) ?? .systemGroupedBackground) 17 | } 18 | 19 | static var secondaryBackground: Color { 20 | Color(UIColor(named: "SecondaryBackground", in: .module, compatibleWith: nil) ?? .systemGroupedBackground) 21 | } 22 | } 23 | 24 | struct Color_Previews: PreviewProvider { 25 | static var previews: some View { 26 | VStack { 27 | preview(for: .text) 28 | preview(for: .primaryBackground) 29 | preview(for: .secondaryBackground) 30 | } 31 | } 32 | 33 | private static func preview(for color: Color) -> some View { 34 | RoundedRectangle(cornerRadius: 12) 35 | .frame(width: 64, height: 64) 36 | .foregroundColor(color) 37 | .background( 38 | RoundedRectangle(cornerRadius: 12) 39 | .inset(by: -2) 40 | .foregroundColor(Color(UIColor.opaqueSeparator)) 41 | ) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Packages/MinimalOnboarding/Sources/MinimalOnboarding/Design/Components/Header.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Header.swift 3 | // 4 | // 5 | // Created by Alex Logan on 31/01/2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct Header: View { 11 | let text: String 12 | let image: String? 13 | 14 | var body: some View { 15 | VStack(alignment: .leading, spacing: DesignConstants.Spacing.doubleCompactStack) { 16 | if let image = image { 17 | Image(uiImage: UIImage.fromPackage(named: image)) 18 | .resizable() 19 | .aspectRatio(contentMode: .fit) 20 | .frame(maxWidth: 160, alignment: .center) 21 | } 22 | Title(text: text) 23 | } 24 | .frame(maxWidth: .infinity, alignment: .leading) 25 | .transition(.opacity) 26 | } 27 | } 28 | 29 | struct Header_Previews: PreviewProvider { 30 | static var previews: some View { 31 | MinimalThemeContainer { 32 | Header(text: "Your login link is on the way!", image: "CoupleWithMoon") 33 | .previewDisplayName("Header with image") 34 | Header(text: "Hey hey", image: nil) 35 | .previewDisplayName("Header without image") 36 | } 37 | .padding() 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Packages/MinimalOnboarding/Sources/MinimalOnboarding/Design/Components/MinimalButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MinimalButton.swift 3 | // 4 | // 5 | // Created by Alex Logan on 30/01/2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct MinimalButton: View { 11 | let text: String 12 | let style: MinimalButton.Style 13 | let prominence: MinimalButton.Prominence 14 | let action: () -> Void 15 | 16 | var body: some View { 17 | Button(action: action, label: { 18 | Text(text) 19 | }) 20 | .buttonStyle(MinimalButtonStyle(style: style, prominence: prominence)) 21 | } 22 | 23 | enum Style { 24 | case primary, secondary 25 | 26 | var backgroundColor: Color { 27 | switch self { 28 | case .primary: 29 | return .text 30 | case .secondary: 31 | return .clear 32 | } 33 | } 34 | 35 | var textColor: Color { 36 | switch self { 37 | case .primary: 38 | return .primaryBackground 39 | case .secondary: 40 | return .text 41 | } 42 | } 43 | } 44 | 45 | enum Prominence { 46 | case regular, reduced 47 | 48 | var height: CGFloat { 49 | switch self { 50 | case .regular: 51 | return 56 52 | case .reduced: 53 | return 24 54 | } 55 | } 56 | } 57 | } 58 | 59 | private struct MinimalButtonStyle: ButtonStyle { 60 | let style: MinimalButton.Style 61 | let prominence: MinimalButton.Prominence 62 | 63 | func makeBody(configuration: Configuration) -> some View { 64 | configuration.label 65 | .typeStyle(.button) 66 | .foregroundColor(style.textColor) 67 | .lineLimit(1) 68 | .padding( 69 | prominence == .regular ? 16 : 0 70 | ) 71 | .frame( 72 | maxWidth: .infinity, 73 | minHeight: prominence.height, 74 | alignment: .center 75 | ) 76 | .background( 77 | Rectangle() 78 | .foregroundColor(style.backgroundColor) 79 | ) 80 | .opacity(configuration.isPressed ? 0.8 : 1) 81 | } 82 | } 83 | 84 | struct MinimalButton_Previews: PreviewProvider { 85 | static var previews: some View { 86 | MinimalThemeContainer { 87 | VStack(spacing: DesignConstants.Spacing.compactStack) { 88 | MinimalButton( 89 | text: "Primary", style: .primary, prominence: .regular, action: {} 90 | ) 91 | MinimalButton( 92 | text: "Secondary", style: .secondary, prominence: .regular, action: {} 93 | ) 94 | MinimalButton( 95 | text: "Primary - Reduced", style: .primary, prominence: .reduced, action: {} 96 | ) 97 | MinimalButton( 98 | text: "Secondary - Reduced", style: .secondary, prominence: .reduced, action: {} 99 | ) 100 | } 101 | .padding() 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /Packages/MinimalOnboarding/Sources/MinimalOnboarding/Design/Components/Title.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Title.swift 3 | // 4 | // 5 | // Created by Alex Logan on 30/01/2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct Title: View { 11 | let text: String 12 | 13 | var body: some View { 14 | Text(text) 15 | .typeStyle(.title) 16 | .foregroundColor(.text) 17 | } 18 | } 19 | 20 | struct Title_Previews: PreviewProvider { 21 | static var previews: some View { 22 | MinimalThemeContainer { 23 | Title(text: "OnboardingKit") 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Packages/MinimalOnboarding/Sources/MinimalOnboarding/Design/ConfirmationCodePage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConfirmationCodePage.swift 3 | // 4 | // 5 | // Created by Alex Logan on 31/01/2023. 6 | // 7 | 8 | import SwiftUI 9 | import SwiftUINavigation 10 | 11 | struct ConfirmationCodePage: View { 12 | let phoneNumber: String 13 | @State var code: String 14 | 15 | @State var keyboardVisible: Bool = false 16 | @State var showAlert: AlertState? 17 | @State var processing: Bool = false 18 | @FocusState var field: Field? 19 | 20 | var actionHandler: (Action) async -> (ActionResult) 21 | 22 | var body: some View { 23 | VStack(alignment: .leading, spacing: DesignConstants.Spacing.doubleCompactStack) { 24 | VStack(alignment: .leading, spacing: DesignConstants.Spacing.doubleCompactStack) { 25 | Header(text: "Verify your phone number", image: nil) 26 | Text("We've sent you a one time verification code to \(phoneNumber)") 27 | .typeStyle(.body) 28 | } 29 | .padding(.top, keyboardVisible ? DesignConstants.Padding.headerPaddingWithKeyboard : DesignConstants.Padding.headerPaddingWihoutNavigationBar) 30 | .frame(maxWidth: .infinity, alignment: .leading) 31 | 32 | if keyboardVisible { Spacer() } 33 | 34 | // This keyboard is hidden with opacity so it still works for every other purpose 35 | // The text is only ever displayed in `CodeDisplayView` 36 | TextField("", text: .init(get: { 37 | return self.code 38 | }, set: { newValue in 39 | self.code = String(newValue.prefix(6)) 40 | })) 41 | .typeStyle(.field) 42 | .keyboardType(.phonePad) 43 | .focused($field, equals: .code) 44 | .disabled(processing) 45 | .opacity(0) 46 | 47 | if !keyboardVisible { Spacer() } 48 | 49 | CodeDisplayView(text: code, isEditing: field == .code) 50 | .onTapGesture { 51 | self.field = .code 52 | } 53 | 54 | HStack { 55 | MinimalButton( 56 | text: "Confirm", 57 | style: .primary, 58 | prominence: .regular, action: { 59 | processing = true 60 | Task { 61 | let result = await actionHandler(.verifyCode(number: code)) 62 | if case .error(let alert) = result { 63 | self.showAlert = alert 64 | } 65 | processing = false 66 | } 67 | }) 68 | .disabled(processing) 69 | } 70 | .overlay( 71 | HStack { 72 | Spacer() 73 | ProgressView() 74 | .progressViewStyle(CircularProgressViewStyle(tint: .primaryBackground)) 75 | } 76 | .padding(.horizontal) 77 | .opacity(processing ? 1 : 0) 78 | ) 79 | } 80 | .animation(.easeInOut, value: keyboardVisible) 81 | .frame(maxWidth: .infinity, alignment: .leading) 82 | .padding() 83 | .onReceive(keyboardPublisher, perform: { keyboardVisible = $0 }) 84 | .alert(unwrapping: $showAlert) { _ in self.showAlert = nil } 85 | .onAppear { 86 | // FocusedField does not always work if you fire on appear, so add a slight delay 87 | DispatchQueue.main.asyncAfter(deadline: .now()+0.2) { 88 | self.field = .code 89 | } 90 | } 91 | } 92 | 93 | enum Action { 94 | case verifyCode(number: String) 95 | } 96 | 97 | enum ActionResult { 98 | case none 99 | case error(alert: AlertState) 100 | } 101 | 102 | enum AlertAction { 103 | case dismiss 104 | } 105 | 106 | enum Field { 107 | case code 108 | } 109 | } 110 | 111 | private struct CodeDisplayView: View { 112 | let text: String 113 | let isEditing: Bool 114 | 115 | /// This could be improved by showing a border around the current field 116 | var body: some View { 117 | HStack { 118 | let currentlyEditingCharacter: String.Index? = isEditing ? text.endIndex : nil 119 | 120 | ForEach(0..<6, id: \.self) { index in 121 | let isEditingThisCharacter = currentlyEditingCharacter?.utf16Offset(in: text) == index 122 | if text.count > index, let character = text[String.Index(utf16Offset: index, in: text)] { 123 | characterDisplay(character: String(character), isEditing: isEditingThisCharacter) 124 | } else { 125 | characterDisplay(character: nil, isEditing: isEditingThisCharacter) 126 | } 127 | } 128 | } 129 | .frame(maxWidth: .infinity) 130 | } 131 | 132 | private func characterDisplay(character: String?, isEditing: Bool) -> some View { 133 | Text(character ?? "") 134 | .font(MinimalFont.Inter.semiBold.font(size: 16, relativeTo: .body)) 135 | .padding() 136 | .frame( 137 | maxWidth: .infinity, 138 | minHeight: 74, 139 | alignment: .center 140 | ) 141 | .background( 142 | RoundedRectangle(cornerRadius: 4) 143 | .foregroundColor(Color.secondaryBackground) 144 | ) 145 | .background( 146 | RoundedRectangle(cornerRadius: 4) 147 | .inset(by: -2) 148 | .foregroundColor(.text) 149 | .opacity(isEditing ? 1 : 0) 150 | ) 151 | .animation(.easeInOut, value: isEditing) 152 | } 153 | } 154 | 155 | struct ConfirmationCodePage_Previews: PreviewProvider { 156 | static var previews: some View { 157 | Preview() 158 | } 159 | 160 | private struct Preview: View { 161 | var body: some View { 162 | MinimalThemeContainer { 163 | ConfirmationCodePage( 164 | phoneNumber: "07375701989", 165 | code: "", 166 | actionHandler: handle(action:) 167 | ) 168 | } 169 | } 170 | 171 | func handle(action: ConfirmationCodePage.Action) async -> ConfirmationCodePage.ActionResult { 172 | return .none 173 | } 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /Packages/MinimalOnboarding/Sources/MinimalOnboarding/Design/DesignConstants.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DesignConstants.swift 3 | // 4 | // 5 | // Created by Alex Logan on 30/01/2023. 6 | // 7 | import UIKit 8 | 9 | enum DesignConstants { 10 | enum Spacing { 11 | static let extraCompactStack: CGFloat = 8 12 | static let compactStack: CGFloat = 12 13 | static let doubleCompactStack: CGFloat = compactStack * 2 14 | } 15 | enum Padding { 16 | static let headerPaddingWihoutNavigationBar: CGFloat = 60 17 | static let headerPaddingWithKeyboard: CGFloat = 60 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Packages/MinimalOnboarding/Sources/MinimalOnboarding/Design/MinimalFont.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MinimalFont.swift 3 | // 4 | // 5 | // Created by Alex Logan on 30/01/2023. 6 | // 7 | 8 | import SwiftUI 9 | import DesignHelpKit 10 | 11 | enum MinimalFont { 12 | enum Inter: CaseIterable, FontDisplayable { 13 | case regular, semiBold 14 | 15 | var name: String { 16 | let baseName = "Inter-" 17 | switch self { 18 | case .regular: 19 | return baseName+"Regular" 20 | case .semiBold: 21 | return baseName+"SemiBold" 22 | } 23 | } 24 | } 25 | 26 | enum Manrope: CaseIterable, FontDisplayable { 27 | case regular, semiBold, extraBold, bold 28 | 29 | var name: String { 30 | let baseName = "Manrope-" 31 | switch self { 32 | case .regular: 33 | return baseName+"Regular" 34 | case .semiBold: 35 | return baseName+"SemiBold" 36 | case .extraBold: 37 | return baseName+"ExtraBold" 38 | case .bold: 39 | return baseName+"Bold" 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Packages/MinimalOnboarding/Sources/MinimalOnboarding/Design/NotificationPermissionPage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NotificationPermissionPage.swift 3 | // 4 | // 5 | // Created by Alex Logan on 31/01/2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct NotificationPermissionPage: View { 11 | var handler: (Action) async -> (NotificationPermissionPage.ActionResult) 12 | 13 | var body: some View { 14 | VStack(alignment: .leading, spacing: DesignConstants.Spacing.compactStack) { 15 | Header(text: "Don't miss anything", image: "LadyWithBag") 16 | VStack(alignment: .leading, spacing: DesignConstants.Spacing.compactStack) { 17 | Text("Get full control over your purchases, allow push notifications and get information when you have made a purchase or when you have something to pay.") 18 | .typeStyle(.body) 19 | } 20 | Spacer() 21 | VStack(spacing: DesignConstants.Spacing.compactStack) { 22 | MinimalButton( 23 | text: "Sure!", 24 | style: .primary, 25 | prominence: .regular, 26 | action: { 27 | Task { 28 | await handler(.requestPermissions) 29 | } 30 | } 31 | ) 32 | MinimalButton( 33 | text: "Not right now", 34 | style: .secondary, 35 | prominence: .reduced, 36 | action: { 37 | Task { 38 | await handler(.skip) 39 | } 40 | } 41 | ) 42 | } 43 | .frame(maxWidth: .infinity, alignment: .center) 44 | } 45 | .frame(maxWidth: .infinity, alignment: .leading) 46 | .padding() 47 | .navigationBarBackButtonHidden(true) 48 | .toolbar(content: { 49 | ToolbarItem(placement: .navigationBarTrailing, content: { 50 | Button(action: { 51 | Task { 52 | await handler(.skip) 53 | } 54 | }, label: { 55 | Image(systemName: "xmark") 56 | .font(.subheadline.weight(.semibold)) 57 | }) 58 | }) 59 | }) 60 | } 61 | 62 | enum Action { 63 | case requestPermissions, skip 64 | } 65 | 66 | enum ActionResult { 67 | // All actions simply push to the next page. 68 | case none 69 | } 70 | } 71 | 72 | struct SwiftUIView_Previews: PreviewProvider { 73 | static var previews: some View { 74 | Preview() 75 | } 76 | private struct Preview: View { 77 | var body: some View { 78 | NavigationStack { 79 | MinimalThemeContainer { 80 | NotificationPermissionPage(handler: handle(action:)) 81 | } 82 | } 83 | } 84 | 85 | func handle(action: NotificationPermissionPage.Action) async -> NotificationPermissionPage.ActionResult { 86 | return .none 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /Packages/MinimalOnboarding/Sources/MinimalOnboarding/Design/SetupConfirmationPage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SetupConfirmationPage.swift 3 | // 4 | // 5 | // Created by Alex Logan on 31/01/2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct SetupConfirmationPage: View { 11 | var handler: (Action) -> Void 12 | 13 | var body: some View { 14 | VStack(alignment: .leading, spacing: DesignConstants.Spacing.compactStack) { 15 | Header(text: "You're all set up!", image: "Flowers") 16 | .padding(.top, DesignConstants.Padding.headerPaddingWihoutNavigationBar) 17 | Text("From now on you can use your phone number to identify yourself, when you log in or confirm transactions.") 18 | .typeStyle(.body) 19 | Spacer() 20 | MinimalButton( 21 | text: "Great", 22 | style: .primary, 23 | prominence: .regular, 24 | action: { 25 | handler(.done) 26 | }) 27 | } 28 | .frame(maxWidth: .infinity, alignment: .leading) 29 | .padding() 30 | .navigationBarBackButtonHidden(true) 31 | } 32 | 33 | enum Action { 34 | case done 35 | } 36 | } 37 | 38 | struct SetupConfirmationPage_Previews: PreviewProvider { 39 | static var previews: some View { 40 | MinimalThemeContainer { 41 | SetupConfirmationPage(handler: { _ in }) 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Packages/MinimalOnboarding/Sources/MinimalOnboarding/Design/Transition.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Transition.swift 3 | // 4 | // 5 | // Created by Alex Logan on 31/01/2023. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | extension AnyTransition { 12 | static var headerTransition: AnyTransition { 13 | .opacity.combined(with: .scale(scale: 0.7)) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Packages/MinimalOnboarding/Sources/MinimalOnboarding/Design/Typography.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Typography.swift 3 | // 4 | // 5 | // Created by Alex Logan on 01/02/2023. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | import DesignHelpKit 11 | 12 | enum Typography { 13 | case title, body, button, field 14 | 15 | var font: Font { 16 | switch self { 17 | case .title: 18 | return MinimalFont.Manrope.bold.font(size: 32, relativeTo: .largeTitle) 19 | case .body: 20 | return MinimalFont.Manrope.regular.font(size: 16, relativeTo: .body) 21 | case .button: 22 | return MinimalFont.Inter.semiBold.font(size: 16, relativeTo: .headline) 23 | case .field: 24 | return MinimalFont.Inter.regular.font(size: 16, relativeTo: .body) 25 | } 26 | } 27 | } 28 | 29 | extension View { 30 | func typeStyle(_ style: Typography) -> some View { 31 | self.font(style.font) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Packages/MinimalOnboarding/Sources/MinimalOnboarding/MinimalThemeContainer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MinimalThemeContainer.swift 3 | // 4 | // 5 | // Created by Alex Logan on 30/01/2023. 6 | // 7 | 8 | import SwiftUI 9 | import DesignHelpKit 10 | 11 | struct MinimalThemeContainer: View { 12 | @StateObject private var model: ContainerModel = ContainerModel() 13 | private let content: () -> (Content) 14 | 15 | init(@ViewBuilder content: @escaping () -> (Content)) { 16 | self.content = content 17 | } 18 | 19 | var body: some View { 20 | content() 21 | .background(Color.primaryBackground.edgesIgnoringSafeArea(.all)) 22 | .accentColor(Color.text) 23 | } 24 | } 25 | 26 | private class ContainerModel: ObservableObject { 27 | init() { 28 | fontUrls(bundle: .module).forEach { fontUrl in 29 | UIFont.register(from: fontUrl) 30 | } 31 | } 32 | 33 | func fontUrls(bundle: Bundle) -> [URL] { 34 | let filenames = MinimalFont.Inter.allCases.map { $0.name } + MinimalFont.Manrope.allCases.map { $0.name } 35 | return filenames.compactMap { 36 | bundle.url(forResource: $0, withExtension: "ttf") 37 | } 38 | } 39 | } 40 | 41 | struct Container_Previews: PreviewProvider { 42 | static var previews: some View { 43 | MinimalThemeContainer { 44 | Text("Manrope") 45 | .font(Font.custom("Manrope-ExtraBold", size: 16)) 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Packages/MinimalOnboarding/Sources/MinimalOnboarding/OnboardingFlow.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OnboardingFlow.swift 3 | // 4 | // 5 | // Created by Alex Logan on 30/01/2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | public struct OnboardingFlow: View { 11 | @StateObject var flowModel: OnboardingFlowModel 12 | 13 | public init( 14 | flowModel: OnboardingFlowModel 15 | ) { 16 | self._flowModel = StateObject(wrappedValue: flowModel) 17 | } 18 | 19 | public var body: some View { 20 | MinimalThemeContainer { 21 | NavigationStack(path: $flowModel.path) { 22 | IntroPage(actionHandler: flowModel.handleIntroActions(action:)) 23 | .applyNavigationDestinations() 24 | } 25 | } 26 | .environmentObject(flowModel) 27 | .onOpenURL(perform: flowModel.handle(url:)) 28 | } 29 | } 30 | 31 | 32 | // MARK: - NavigationStack helper 33 | struct FlowNavigationModifier: ViewModifier { 34 | @EnvironmentObject var flowModel: OnboardingFlowModel 35 | 36 | func body(content: Content) -> some View { 37 | content 38 | .navigationDestination(for: Screen.self, destination: { screen in 39 | switch screen { 40 | case .signUp: 41 | EmailPage( 42 | email: flowModel.state.email ?? "", 43 | toggleOn: flowModel.state.rememberDetails, 44 | actionHandler: flowModel.handleEmailActions(action:) 45 | ) 46 | case .signIn, .phoneNumberEntry: 47 | PhoneNumberEntryPage( 48 | phoneNumber: flowModel.state.phoneNumber ?? "", 49 | actionHandler: flowModel.handlePhoneActions(action:) 50 | ) 51 | case .emailVerification: 52 | ConfirmEmailPage( 53 | email: flowModel.state.email ?? "" 54 | ) 55 | case .intro: 56 | IntroPage(actionHandler: flowModel.handleIntroActions(action:)) 57 | case .promotion: 58 | PromotionPage(actionHandler: flowModel.handlePromoAction(action:)) 59 | case .phoneNumberVerification: 60 | ConfirmationCodePage( 61 | phoneNumber: flowModel.state.phoneNumber ?? "", 62 | code: flowModel.state.enteredCode ?? "", 63 | actionHandler: flowModel.handleConfirmationCodeActions(action:) 64 | ) 65 | case .phoneConfirmed: 66 | SetupConfirmationPage(handler: flowModel.handleSetupConfirmationActions(action:)) 67 | case .notifications: 68 | NotificationPermissionPage(handler: flowModel.handleNotificationActions(action:)) 69 | } 70 | }) 71 | } 72 | } 73 | 74 | extension View { 75 | func applyNavigationDestinations() -> some View { 76 | self 77 | .modifier(FlowNavigationModifier()) 78 | } 79 | } 80 | 81 | private struct OnboardingFlow_Previews: PreviewProvider { 82 | static var previews: some View { 83 | OnboardingFlow( 84 | flowModel: OnboardingFlowModel( 85 | path: [.signIn, .signUp, .emailVerification], handler: { 86 | _ in 87 | } 88 | ) 89 | ) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /Packages/MinimalOnboarding/Sources/MinimalOnboarding/OnboardingFlowModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OnboardingFlowModel.swift 3 | // 4 | // 5 | // Created by Alex Logan on 31/01/2023. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | import SwiftUINavigation 11 | 12 | public class OnboardingFlowModel: ObservableObject { 13 | private let authService: AuthService 14 | private let userService: UserService 15 | private let validationService: ContactValidating 16 | private let linkParser: LinkParsing 17 | private let notificationPermissionProvider: NotificationPermissionProviding 18 | 19 | @Published var path: [Screen] 20 | @Published var state: FlowState 21 | 22 | let handler: (OnboardingAction) -> Void 23 | 24 | public init( 25 | path: [Screen] = [], 26 | state: FlowState = .init(rememberDetails: false), 27 | authService: AuthService = PlaceholderAuthService(), 28 | userService: UserService = AppStorageUserService(), 29 | validationService: ContactValidating = ContactValidator(), 30 | linkParser: LinkParsing = LinkParser(), 31 | notificationPermissionProvider: NotificationPermissionProviding = NotificationPermissionProvider(), 32 | handler: @escaping (OnboardingAction) -> Void 33 | ) { 34 | self.path = path 35 | self.state = state 36 | self.authService = authService 37 | self.userService = userService 38 | self.validationService = validationService 39 | self.linkParser = linkParser 40 | self.notificationPermissionProvider = notificationPermissionProvider 41 | self.handler = handler 42 | } 43 | 44 | func handle(url: URL) { 45 | switch linkParser.validate(url: url) { 46 | case .emailVerification(let email): 47 | // If we're mid flow, make sure we only verify the email we expect 48 | if let stateEmail = state.email, stateEmail != email { return } 49 | state.email = email 50 | if (path.contains(.emailVerification) || path.isEmpty) && !path.contains(.promotion) { 51 | path.append(.promotion) 52 | } 53 | case .none: return 54 | } 55 | } 56 | 57 | // MARK: - Intro Actions 58 | func handleIntroActions(action: IntroPage.Action) { 59 | switch action { 60 | case .signIn: 61 | state.flow = .signIn 62 | path.append(.signIn) 63 | case .signUp: 64 | state.flow = .signUp 65 | path.append(.signUp) 66 | } 67 | } 68 | 69 | // MARK: - Email Actions 70 | @MainActor 71 | func handleEmailActions(action: EmailPage.Action) async -> EmailPage.ActionResult { 72 | switch action { 73 | case .sendLink(let email): 74 | guard validationService.validateEmail(string: email) else { 75 | return .error(alert: makeSimpleAlert(title: "Error", body: "Please enter a valid email.", dismissAction: .dismiss)) 76 | } 77 | if state.rememberDetails { userService.store(email: email) } 78 | state.email = email 79 | let result = await authService.sendValidationEmail(to: email) 80 | if result { 81 | path.append(.emailVerification) 82 | return .none 83 | } else { 84 | return .error(alert: makeSimpleAlert(title: "Error", body: "Your email could not be verified at this time", dismissAction: .dismiss)) 85 | } 86 | case .toggleRememberDetails(let remember): 87 | if !remember { userService.clear() } 88 | return .none 89 | } 90 | } 91 | 92 | // MARK: - Promo actions 93 | func handlePromoAction(action: PromotionPage.Action) { 94 | path.append(.phoneNumberEntry) 95 | } 96 | 97 | // MARK: - Phone Number Actions 98 | @MainActor 99 | func handlePhoneActions(action: PhoneNumberEntryPage.Action) async -> PhoneNumberEntryPage.ActionResult { 100 | switch action { 101 | case .sendMessage(let phoneNumber): 102 | guard validationService.validatePhoneNumber(string: phoneNumber) else { 103 | return .error(alert: makeSimpleAlert(title: "Error", body: "Please enter a valid phone number.", dismissAction: .dismiss)) 104 | } 105 | if state.rememberDetails { userService.store(phoneNumber: phoneNumber) } 106 | state.phoneNumber = phoneNumber 107 | if let result = await authService.sendValidationCode(to: phoneNumber) { 108 | state.expectedCode = result 109 | path.append(.phoneNumberVerification) 110 | return .none 111 | } else { 112 | return .error(alert: makeSimpleAlert(title: "Error", body: "Your phone number could not be verified at this time", dismissAction: .dismiss)) 113 | } 114 | } 115 | } 116 | 117 | // MARK: - Phone Validation Actions 118 | @MainActor 119 | func handleConfirmationCodeActions(action: ConfirmationCodePage.Action) async -> ConfirmationCodePage.ActionResult { 120 | switch action { 121 | case .verifyCode(let number): 122 | self.state.enteredCode = number 123 | guard let code = state.expectedCode else { 124 | // An improvement could be made that this provides an action to re-send a code. 125 | return .error(alert: makeSimpleAlert(title: "Error", body: "Something has gone wrong. Please re-enter your phone number.", dismissAction: .dismiss)) 126 | } 127 | guard code == number else { 128 | return .error(alert: makeSimpleAlert(title: "Error", body: "That's not the code we were expecting.", dismissAction: .dismiss)) 129 | } 130 | switch state.flow { 131 | case .signIn: 132 | path.append(.notifications) 133 | case .signUp: 134 | path.append(.phoneConfirmed) 135 | case .none: 136 | break 137 | } 138 | return .none 139 | } 140 | } 141 | 142 | // MARK: - Setup Confirmation Actions 143 | func handleSetupConfirmationActions(action: SetupConfirmationPage.Action) { 144 | switch action { 145 | case .done: 146 | self.path.append(.notifications) 147 | } 148 | } 149 | 150 | // MARK: - Notification Actions 151 | @MainActor 152 | func handleNotificationActions(action: NotificationPermissionPage.Action) async -> NotificationPermissionPage.ActionResult { 153 | switch action { 154 | case .requestPermissions: 155 | _ = await notificationPermissionProvider.requestPermission() 156 | handler(.complete) 157 | return .none 158 | case .skip: 159 | handler(.complete) 160 | return .none 161 | } 162 | } 163 | 164 | // MARK: - AlertHelper 165 | private func makeSimpleAlert(title: String, body: String, dismissAction: Action) -> AlertState { 166 | AlertState(title: { 167 | TextState(verbatim: title) 168 | }, actions: { 169 | ButtonState(role: .cancel, action: .send(dismissAction)) { 170 | TextState("Dismiss") 171 | } 172 | }, message: { 173 | TextState(body) 174 | }) 175 | } 176 | 177 | // MARK: - State 178 | public struct FlowState { 179 | var flow: Flow? 180 | var rememberDetails: Bool 181 | var email: String? 182 | var phoneNumber: String? 183 | var enteredCode: String? 184 | var expectedCode: String? 185 | 186 | public init(flow: OnboardingFlowModel.FlowState.Flow? = nil, rememberDetails: Bool = false, email: String? = nil, phoneNumber: String? = nil, enteredCode: String? = nil, expectedCode: String? = nil) { 187 | self.flow = flow 188 | self.rememberDetails = rememberDetails 189 | self.email = email 190 | self.phoneNumber = phoneNumber 191 | self.enteredCode = enteredCode 192 | self.expectedCode = expectedCode 193 | } 194 | 195 | public enum Flow { 196 | case signIn, signUp 197 | } 198 | } 199 | 200 | public enum OnboardingAction { 201 | case complete 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /Packages/MinimalOnboarding/Sources/MinimalOnboarding/Pages/ConfirmEmailPage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConfirmEmailPage.swift 3 | // 4 | // 5 | // Created by Alex Logan on 31/01/2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /// For an easy way to test this page, inside the overall flow, run `xcrun simctl openurl booted 'minimal-onboarding://email-verification?email=alex@alex.com'`. 11 | /// You'll have to ensure the email provided in the URL matches the URL you've already entered, if you have gone via the email page. 12 | struct ConfirmEmailPage: View { 13 | let email: String 14 | 15 | var body: some View { 16 | VStack(alignment: .leading, spacing: DesignConstants.Spacing.doubleCompactStack) { 17 | Header(text: "Your login link is on the way!", image: "Drum") 18 | VStack(alignment: .leading, spacing: DesignConstants.Spacing.compactStack) { 19 | Text("We've sent an email with a verification link to \(email).") 20 | .typeStyle(.body) 21 | Text("If you're unable to find the email, please check your spam folder") 22 | .typeStyle(.body) 23 | } 24 | Spacer() 25 | } 26 | .frame(maxWidth: .infinity, alignment: .leading) 27 | .padding() 28 | } 29 | } 30 | 31 | struct ConfirmEmailPage_Previews: PreviewProvider { 32 | static var previews: some View { 33 | MinimalThemeContainer { 34 | ConfirmEmailPage(email: "alex@unflow.com") 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Packages/MinimalOnboarding/Sources/MinimalOnboarding/Pages/EmailPage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EmailPage.swift 3 | // 4 | // 5 | // Created by Alex Logan on 30/01/2023. 6 | // 7 | 8 | import SwiftUI 9 | import SwiftUINavigation 10 | 11 | struct EmailPage: View { 12 | @State var email: String = "" 13 | @State var toggleOn: Bool = false 14 | 15 | @State var keyboardVisible: Bool = false 16 | @State var showAlert: AlertState? 17 | @State var processing: Bool = false 18 | 19 | var actionHandler: (Action) async -> (ActionResult) 20 | 21 | var body: some View { 22 | VStack(alignment: .leading, spacing: DesignConstants.Spacing.doubleCompactStack) { 23 | if !keyboardVisible { 24 | VStack(alignment: .center, spacing: DesignConstants.Spacing.doubleCompactStack) { 25 | HStack { 26 | Image(uiImage: UIImage.fromPackage(named: "CoupleWithMoon")) 27 | .resizable() 28 | .aspectRatio(1.0, contentMode: .fit) 29 | .frame(maxWidth: 260) 30 | } 31 | .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) 32 | Title(text: "Sign up in less than 2 minutes") 33 | .frame(maxWidth: .infinity, alignment: .leading) 34 | } 35 | .frame(maxWidth: .infinity, alignment: .center) 36 | .transition(.headerTransition) 37 | } else { 38 | Spacer() 39 | } 40 | 41 | TextField("Email Address", text: $email) 42 | .textContentType(.emailAddress) 43 | .typeStyle(.field) 44 | .padding() 45 | .background(Color.secondaryBackground) 46 | .disabled(processing) 47 | 48 | HStack { 49 | MinimalButton( 50 | text: "Send link", 51 | style: .primary, 52 | prominence: .regular, action: { 53 | processing = true 54 | Task { 55 | let result = await actionHandler(.sendLink(email: email.lowercased())) 56 | if case .error(let alert) = result { 57 | self.showAlert = alert 58 | } 59 | processing = false 60 | } 61 | }) 62 | .disabled(processing) 63 | } 64 | .overlay( 65 | HStack { 66 | Spacer() 67 | ProgressView() 68 | .progressViewStyle(CircularProgressViewStyle(tint: .primaryBackground)) 69 | } 70 | .padding(.horizontal) 71 | .opacity(processing ? 1 : 0) 72 | ) 73 | 74 | detailsToggle 75 | } 76 | .animation(.easeInOut, value: keyboardVisible) 77 | .frame(maxWidth: .infinity, alignment: .leading) 78 | .padding() 79 | .onReceive(keyboardPublisher, perform: { keyboardVisible = $0 }) 80 | .alert(unwrapping: $showAlert) { _ in self.showAlert = nil } 81 | } 82 | 83 | var detailsToggle: some View { 84 | HStack { 85 | Text("Remember login details") 86 | .font(MinimalFont.Manrope.regular.font(size: 12, relativeTo: .caption)) 87 | Spacer() 88 | Toggle(isOn: $toggleOn.sideEffect(onSet: { (newValue, _) in 89 | Task { 90 | _ = await actionHandler(.toggleRememberDetails(remember: newValue)) 91 | } 92 | }), label: { }) 93 | .labelsHidden() 94 | .toggleStyle(SwitchToggleStyle(tint: Color.text) 95 | ) 96 | } 97 | } 98 | 99 | enum Action { 100 | case sendLink(email: String) 101 | case toggleRememberDetails(remember: Bool) 102 | } 103 | 104 | enum ActionResult { 105 | case none 106 | case error(alert: AlertState) 107 | } 108 | 109 | enum AlertAction { 110 | case dismiss 111 | } 112 | } 113 | 114 | struct EmailPage_Previews: PreviewProvider { 115 | static var previews: some View { 116 | Preview() 117 | } 118 | 119 | private struct Preview: View { 120 | var body: some View { 121 | MinimalThemeContainer { 122 | EmailPage(actionHandler: handle(action:)) 123 | } 124 | } 125 | 126 | func handle(action: EmailPage.Action) async -> (EmailPage.ActionResult) { 127 | return .none 128 | } 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /Packages/MinimalOnboarding/Sources/MinimalOnboarding/Pages/IntroPage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IntroView.swift 3 | // 4 | // 5 | // Created by Alex Logan on 30/01/2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct IntroPage: View { 11 | let actionHandler: ((IntroPage.Action) -> Void) 12 | 13 | var body: some View { 14 | VStack(alignment: .leading, spacing: DesignConstants.Spacing.doubleCompactStack) { 15 | Title(text: "Boost your business risk free") 16 | .padding(.top, DesignConstants.Padding.headerPaddingWihoutNavigationBar) 17 | .padding(.horizontal) 18 | .minimumScaleFactor(0.6) 19 | 20 | OffsetImageStack( 21 | bottomLeadingImageName: "IntroImageOne", 22 | topTrailingimageName: "IntroImageTwo" 23 | ) 24 | .accessibilityHidden(true) 25 | 26 | VStack(spacing: DesignConstants.Spacing.compactStack) { 27 | MinimalButton( 28 | text: "Sign Up", 29 | style: .primary, 30 | prominence: .regular, 31 | action: { 32 | actionHandler(.signUp) 33 | } 34 | ) 35 | MinimalButton( 36 | text: "Log In", 37 | style: .secondary, 38 | prominence: .regular, 39 | action: { 40 | actionHandler(.signIn) 41 | } 42 | ) 43 | } 44 | .padding([.horizontal, .top]) 45 | .padding(.bottom, DesignConstants.Spacing.doubleCompactStack) 46 | } 47 | .navigationBarHidden(true) 48 | .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading) 49 | } 50 | 51 | enum Action { 52 | case signIn, signUp 53 | } 54 | } 55 | 56 | /// Double image stack that shows two images, one at the bottom leading edge and one at the top trailing 57 | /// If the user has large type sizes enabled, we only show one image, the one provided as bottomLeadingImage 58 | private struct OffsetImageStack: View { 59 | @Environment(\.sizeCategory) var sizeCategory 60 | let bottomLeadingImageName: String 61 | let topTrailingimageName: String 62 | 63 | var isRegularSizes: Bool { 64 | sizeCategory < .extraLarge 65 | } 66 | 67 | var stackAligmment: VerticalAlignment { 68 | isRegularSizes ? .top : .center 69 | } 70 | 71 | var body: some View { 72 | HStack(alignment: stackAligmment, spacing: DesignConstants.Spacing.doubleCompactStack) { 73 | Image(uiImage: .fromPackage(named: bottomLeadingImageName)) 74 | .resizable() 75 | .aspectRatio(0.6, contentMode: .fit) 76 | .alignmentGuide(.top) { context in 77 | return context[.top] - context.height / 2 78 | } 79 | 80 | if isRegularSizes { 81 | Image(uiImage: .fromPackage(named: topTrailingimageName)) 82 | .resizable() 83 | .aspectRatio(0.6, contentMode: .fit) 84 | } 85 | } 86 | .frame( 87 | maxWidth: .infinity, 88 | maxHeight: .infinity // Cap to stop the view from ever going outside the bounds of the page 89 | ) 90 | } 91 | } 92 | 93 | struct IntroView_Previews: PreviewProvider { 94 | static var previews: some View { 95 | MinimalThemeContainer { 96 | IntroPage(actionHandler: { _ in }) 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /Packages/MinimalOnboarding/Sources/MinimalOnboarding/Pages/PhoneNumberEntryPage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PhoneNumberEntryPage.swift 3 | // 4 | // 5 | // Created by Alex Logan on 31/01/2023. 6 | // 7 | 8 | import SwiftUI 9 | import SwiftUINavigation 10 | 11 | struct PhoneNumberEntryPage: View { 12 | @State var phoneNumber: String 13 | 14 | @State var keyboardVisible: Bool = false 15 | @State var showAlert: AlertState? 16 | @State var processing: Bool = false 17 | 18 | var actionHandler: (Action) async -> (ActionResult) 19 | 20 | var body: some View { 21 | VStack(alignment: .leading, spacing: DesignConstants.Spacing.doubleCompactStack) { 22 | if !keyboardVisible { 23 | VStack(alignment: .leading, spacing: DesignConstants.Spacing.doubleCompactStack) { 24 | Header(text: "What's your phone number?", image: "StrangeEyes") 25 | Text("We need to make sure you're you. Please let us know what number to send a code to.") 26 | .typeStyle(.body) 27 | } 28 | .frame(maxWidth: .infinity, alignment: .leading) 29 | .transition(.headerTransition) 30 | } 31 | 32 | if keyboardVisible { Spacer() } 33 | 34 | TextField("Phone Number", text: $phoneNumber) 35 | .typeStyle(.field) 36 | .padding() 37 | .keyboardType(.phonePad) 38 | .textContentType(.telephoneNumber) 39 | .background(Color.secondaryBackground) 40 | .disabled(processing) 41 | 42 | if !keyboardVisible { Spacer() } 43 | 44 | HStack { 45 | MinimalButton( 46 | text: "Send code", 47 | style: .primary, 48 | prominence: .regular, action: { 49 | processing = true 50 | Task { 51 | let result = await actionHandler(.sendMessage(number: phoneNumber)) 52 | if case .error(let alert) = result { 53 | self.showAlert = alert 54 | } 55 | processing = false 56 | } 57 | }) 58 | .disabled(processing) 59 | } 60 | .overlay( 61 | HStack { 62 | Spacer() 63 | ProgressView() 64 | .progressViewStyle(CircularProgressViewStyle(tint: .primaryBackground)) 65 | } 66 | .padding(.horizontal) 67 | .opacity(processing ? 1 : 0) 68 | ) 69 | } 70 | .animation(.easeInOut, value: keyboardVisible) 71 | .frame(maxWidth: .infinity, alignment: .leading) 72 | .padding() 73 | .onReceive(keyboardPublisher, perform: { keyboardVisible = $0 }) 74 | .alert(unwrapping: $showAlert) { _ in self.showAlert = nil } 75 | } 76 | 77 | enum Action { 78 | case sendMessage(number: String) 79 | } 80 | 81 | enum ActionResult { 82 | case none 83 | case error(alert: AlertState) 84 | } 85 | 86 | enum AlertAction { 87 | case dismiss 88 | } 89 | } 90 | 91 | struct PhoneNumberEntryPage_Previews: PreviewProvider { 92 | static var previews: some View { 93 | Preview() 94 | } 95 | 96 | private struct Preview: View { 97 | var body: some View { 98 | MinimalThemeContainer { 99 | PhoneNumberEntryPage( 100 | phoneNumber: "", 101 | actionHandler: handle(action:) 102 | ) 103 | } 104 | } 105 | 106 | func handle(action: PhoneNumberEntryPage.Action) async -> PhoneNumberEntryPage.ActionResult { 107 | return .none 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /Packages/MinimalOnboarding/Sources/MinimalOnboarding/Pages/PromotionPage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PromotionController.swift 3 | // 4 | // 5 | // Created by Alex Logan on 30/01/2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct PromotionPage: View { 11 | let actionHandler: ((PromotionPage.Action) -> Void) 12 | var items: [PromotionItem] = PromotionItem.all 13 | 14 | @State var index: Int = 0 15 | 16 | var body: some View { 17 | VStack(spacing: DesignConstants.Spacing.compactStack) { 18 | TabView(selection: $index) { 19 | ForEach(Array(items.enumerated()), id: \.offset) { item in 20 | PromotionPageElement(item: item.element) 21 | .tag(item.offset) 22 | } 23 | } 24 | .tabViewStyle(PageTabViewStyle(indexDisplayMode: .never)) // Hide the default so we can control the position exactly 25 | // NB: We can't ignore the safe area like the design due to a bug it causes with layout in iOS 16. 26 | // You will get a bunch of errors to do with collection view layouts, which TabView with PageTabViewStyle uses under the hood. 27 | // To re-create, uncomment either of these lines, and scroll. 28 | // If a solution is found for this, a PR would be welcome :) 29 | // .edgesIgnoringSafeArea(.top) 30 | // .ignoresSafeArea(.container, edges: .top) 31 | .padding(.bottom, DesignConstants.Spacing.compactStack) 32 | 33 | PageControl(numberOfPages: items.count, currentPage: $index) 34 | 35 | MinimalButton( 36 | text: index == (items.endIndex-1) ? "Done" : "Next", 37 | style: .primary, 38 | prominence: .regular, 39 | action: increment 40 | ) 41 | .padding() 42 | } 43 | .animation(.interactiveSpring(), value: index) 44 | .toolbar(.hidden, for: .navigationBar) 45 | } 46 | 47 | private func increment() { 48 | if index < (items.endIndex-1) { 49 | index += 1 50 | } else { 51 | actionHandler(.complete) 52 | } 53 | } 54 | 55 | enum Action { 56 | case complete 57 | } 58 | } 59 | 60 | /// Replace your promotion items with real content 61 | struct PromotionItem { 62 | let title: String 63 | let text: String 64 | let imageName: String 65 | 66 | static let all = [one, two] 67 | static let one: PromotionItem = .init(title: "Lorem ipsum dolor sit amet", text: "Lorem ipsum dolor sit amet consectetur. Amet massa duis pellentesque eu.", imageName: "PromotionImageOne") 68 | static let two: PromotionItem = .init(title: "Lorem ipsum dolor sit amet", text: "Lorem ipsum dolor sit amet consectetur. Amet massa duis pellentesque eu.", imageName: "PromotionImageTwo") 69 | } 70 | 71 | private struct PromotionPageElement: View { 72 | let item: PromotionItem 73 | 74 | var body: some View { 75 | VStack(alignment: .leading, spacing: 40) { 76 | Rectangle() 77 | .foregroundColor(.clear) 78 | .frame(maxHeight: .infinity, alignment: .center) 79 | .background( 80 | Image(uiImage: .fromPackage(named: item.imageName)) 81 | .resizable() 82 | .aspectRatio(contentMode: .fill) 83 | ) 84 | .clipped() 85 | 86 | VStack(alignment: .leading, spacing: DesignConstants.Spacing.extraCompactStack) { 87 | Text(item.title) 88 | .font(MinimalFont.Manrope.extraBold.font(size: 24, relativeTo: .title)) 89 | Text(item.text) 90 | .typeStyle(.body) 91 | } 92 | .padding(.horizontal) 93 | } 94 | .frame(maxWidth: .infinity, alignment: .leading) 95 | } 96 | } 97 | 98 | private struct PageControl: UIViewRepresentable { 99 | var numberOfPages: Int 100 | @Binding var currentPage: Int 101 | 102 | func makeCoordinator() -> Coordinator { 103 | Coordinator(self) 104 | } 105 | 106 | func makeUIView(context: Context) -> UIPageControl { 107 | let control = UIPageControl() 108 | control.numberOfPages = numberOfPages 109 | control.pageIndicatorTintColor = UIColor.secondaryLabel 110 | control.currentPageIndicatorTintColor = UIColor.label 111 | 112 | control.addTarget( 113 | context.coordinator, 114 | action: #selector(Coordinator.updateCurrentPage(sender:)), 115 | for: .valueChanged 116 | ) 117 | 118 | return control 119 | } 120 | 121 | func updateUIView(_ uiView: UIPageControl, context: Context) { 122 | uiView.currentPage = currentPage 123 | } 124 | 125 | class Coordinator: NSObject { 126 | var control: PageControl 127 | 128 | init(_ control: PageControl) { 129 | self.control = control 130 | } 131 | @objc 132 | func updateCurrentPage(sender: UIPageControl) { 133 | control.currentPage = sender.currentPage 134 | } 135 | } 136 | } 137 | 138 | struct PromotionController_Previews: PreviewProvider { 139 | static var previews: some View { 140 | MinimalThemeContainer { 141 | PromotionPage(actionHandler: { _ in }) 142 | } 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /Packages/MinimalOnboarding/Sources/MinimalOnboarding/Resources/Assets.xcassets/Colors/Background.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "display-p3", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "1.000", 9 | "green" : "0.992", 10 | "red" : "1.000" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "display-p3", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0.055", 27 | "green" : "0.055", 28 | "red" : "0.055" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Packages/MinimalOnboarding/Sources/MinimalOnboarding/Resources/Assets.xcassets/Colors/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Packages/MinimalOnboarding/Sources/MinimalOnboarding/Resources/Assets.xcassets/Colors/SecondaryBackground.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.965", 9 | "green" : "0.965", 10 | "red" : "0.965" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0.200", 27 | "green" : "0.200", 28 | "red" : "0.200" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Packages/MinimalOnboarding/Sources/MinimalOnboarding/Resources/Assets.xcassets/Colors/Text.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.098", 9 | "green" : "0.086", 10 | "red" : "0.094" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0.902", 27 | "green" : "0.914", 28 | "red" : "0.906" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Packages/MinimalOnboarding/Sources/MinimalOnboarding/Resources/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Packages/MinimalOnboarding/Sources/MinimalOnboarding/Resources/Assets.xcassets/Images/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Packages/MinimalOnboarding/Sources/MinimalOnboarding/Resources/Assets.xcassets/Images/CoupleWithMoon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "file1.svg", 5 | "idiom" : "universal" 6 | }, 7 | { 8 | "appearances" : [ 9 | { 10 | "appearance" : "luminosity", 11 | "value" : "dark" 12 | } 13 | ], 14 | "filename" : "file1 (1).svg", 15 | "idiom" : "universal" 16 | } 17 | ], 18 | "info" : { 19 | "author" : "xcode", 20 | "version" : 1 21 | }, 22 | "properties" : { 23 | "template-rendering-intent" : "original" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Packages/MinimalOnboarding/Sources/MinimalOnboarding/Resources/Assets.xcassets/Images/Drum.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "file1.svg", 5 | "idiom" : "universal" 6 | }, 7 | { 8 | "appearances" : [ 9 | { 10 | "appearance" : "luminosity", 11 | "value" : "dark" 12 | } 13 | ], 14 | "filename" : "file1 (3).svg", 15 | "idiom" : "universal" 16 | } 17 | ], 18 | "info" : { 19 | "author" : "xcode", 20 | "version" : 1 21 | }, 22 | "properties" : { 23 | "template-rendering-intent" : "original" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Packages/MinimalOnboarding/Sources/MinimalOnboarding/Resources/Assets.xcassets/Images/Drum.imageset/file1 (3).svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /Packages/MinimalOnboarding/Sources/MinimalOnboarding/Resources/Assets.xcassets/Images/Drum.imageset/file1.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | ]> 13 | 16 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 30 | 32 | 33 | 34 | 35 | 36 | 37 | 47 | 68 | 88 | 95 | 102 | 104 | 105 | 106 | 107 | 109 | 110 | 111 | 119 | 127 | 134 | 143 | 145 | 152 | 159 | 165 | 167 | 174 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 198 | 205 | 212 | 213 | 214 | 215 | 217 | 218 | 226 | 234 | 241 | 242 | 243 | 257 | 297 | 298 | 301 | 302 | 303 | 304 | -------------------------------------------------------------------------------- /Packages/MinimalOnboarding/Sources/MinimalOnboarding/Resources/Assets.xcassets/Images/Flowers.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "file1.svg", 5 | "idiom" : "universal" 6 | }, 7 | { 8 | "appearances" : [ 9 | { 10 | "appearance" : "luminosity", 11 | "value" : "dark" 12 | } 13 | ], 14 | "filename" : "file1 (4).svg", 15 | "idiom" : "universal" 16 | } 17 | ], 18 | "info" : { 19 | "author" : "xcode", 20 | "version" : 1 21 | }, 22 | "properties" : { 23 | "template-rendering-intent" : "original" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Packages/MinimalOnboarding/Sources/MinimalOnboarding/Resources/Assets.xcassets/Images/IntroImageOne.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "image 79.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Packages/MinimalOnboarding/Sources/MinimalOnboarding/Resources/Assets.xcassets/Images/IntroImageOne.imageset/image 79.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unflowhq/OnboardingKit/a4358c615697dea86205bc0788bcd48419abc583/Packages/MinimalOnboarding/Sources/MinimalOnboarding/Resources/Assets.xcassets/Images/IntroImageOne.imageset/image 79.png -------------------------------------------------------------------------------- /Packages/MinimalOnboarding/Sources/MinimalOnboarding/Resources/Assets.xcassets/Images/IntroImageTwo.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "image 78.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Packages/MinimalOnboarding/Sources/MinimalOnboarding/Resources/Assets.xcassets/Images/IntroImageTwo.imageset/image 78.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unflowhq/OnboardingKit/a4358c615697dea86205bc0788bcd48419abc583/Packages/MinimalOnboarding/Sources/MinimalOnboarding/Resources/Assets.xcassets/Images/IntroImageTwo.imageset/image 78.png -------------------------------------------------------------------------------- /Packages/MinimalOnboarding/Sources/MinimalOnboarding/Resources/Assets.xcassets/Images/LadyWithBag.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "file0.svg", 5 | "idiom" : "universal" 6 | }, 7 | { 8 | "appearances" : [ 9 | { 10 | "appearance" : "luminosity", 11 | "value" : "dark" 12 | } 13 | ], 14 | "filename" : "file1.svg", 15 | "idiom" : "universal" 16 | } 17 | ], 18 | "info" : { 19 | "author" : "xcode", 20 | "version" : 1 21 | }, 22 | "properties" : { 23 | "template-rendering-intent" : "original" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Packages/MinimalOnboarding/Sources/MinimalOnboarding/Resources/Assets.xcassets/Images/PromotionImageOne.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "image 80.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Packages/MinimalOnboarding/Sources/MinimalOnboarding/Resources/Assets.xcassets/Images/PromotionImageOne.imageset/image 80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unflowhq/OnboardingKit/a4358c615697dea86205bc0788bcd48419abc583/Packages/MinimalOnboarding/Sources/MinimalOnboarding/Resources/Assets.xcassets/Images/PromotionImageOne.imageset/image 80.png -------------------------------------------------------------------------------- /Packages/MinimalOnboarding/Sources/MinimalOnboarding/Resources/Assets.xcassets/Images/PromotionImageTwo.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "image 80-1.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Packages/MinimalOnboarding/Sources/MinimalOnboarding/Resources/Assets.xcassets/Images/PromotionImageTwo.imageset/image 80-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unflowhq/OnboardingKit/a4358c615697dea86205bc0788bcd48419abc583/Packages/MinimalOnboarding/Sources/MinimalOnboarding/Resources/Assets.xcassets/Images/PromotionImageTwo.imageset/image 80-1.png -------------------------------------------------------------------------------- /Packages/MinimalOnboarding/Sources/MinimalOnboarding/Resources/Assets.xcassets/Images/StrangeEyes.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "file1.svg", 5 | "idiom" : "universal" 6 | }, 7 | { 8 | "appearances" : [ 9 | { 10 | "appearance" : "luminosity", 11 | "value" : "dark" 12 | } 13 | ], 14 | "filename" : "file1 (2).svg", 15 | "idiom" : "universal" 16 | } 17 | ], 18 | "info" : { 19 | "author" : "xcode", 20 | "version" : 1 21 | }, 22 | "properties" : { 23 | "template-rendering-intent" : "original" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Packages/MinimalOnboarding/Sources/MinimalOnboarding/Resources/Fonts/Inter-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unflowhq/OnboardingKit/a4358c615697dea86205bc0788bcd48419abc583/Packages/MinimalOnboarding/Sources/MinimalOnboarding/Resources/Fonts/Inter-Regular.ttf -------------------------------------------------------------------------------- /Packages/MinimalOnboarding/Sources/MinimalOnboarding/Resources/Fonts/Inter-SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unflowhq/OnboardingKit/a4358c615697dea86205bc0788bcd48419abc583/Packages/MinimalOnboarding/Sources/MinimalOnboarding/Resources/Fonts/Inter-SemiBold.ttf -------------------------------------------------------------------------------- /Packages/MinimalOnboarding/Sources/MinimalOnboarding/Resources/Fonts/Manrope-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unflowhq/OnboardingKit/a4358c615697dea86205bc0788bcd48419abc583/Packages/MinimalOnboarding/Sources/MinimalOnboarding/Resources/Fonts/Manrope-Bold.ttf -------------------------------------------------------------------------------- /Packages/MinimalOnboarding/Sources/MinimalOnboarding/Resources/Fonts/Manrope-ExtraBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unflowhq/OnboardingKit/a4358c615697dea86205bc0788bcd48419abc583/Packages/MinimalOnboarding/Sources/MinimalOnboarding/Resources/Fonts/Manrope-ExtraBold.ttf -------------------------------------------------------------------------------- /Packages/MinimalOnboarding/Sources/MinimalOnboarding/Resources/Fonts/Manrope-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unflowhq/OnboardingKit/a4358c615697dea86205bc0788bcd48419abc583/Packages/MinimalOnboarding/Sources/MinimalOnboarding/Resources/Fonts/Manrope-Regular.ttf -------------------------------------------------------------------------------- /Packages/MinimalOnboarding/Sources/MinimalOnboarding/Resources/Fonts/Manrope-SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unflowhq/OnboardingKit/a4358c615697dea86205bc0788bcd48419abc583/Packages/MinimalOnboarding/Sources/MinimalOnboarding/Resources/Fonts/Manrope-SemiBold.ttf -------------------------------------------------------------------------------- /Packages/MinimalOnboarding/Sources/MinimalOnboarding/Resources/UIImage+MinimalOnboarding.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIImage+MinimalOnboarding.swift 3 | // 4 | // 5 | // Created by Alex Logan on 30/01/2023. 6 | // 7 | 8 | import DesignHelpKit 9 | import UIKit 10 | 11 | /// Convenience wrapper to get an image from the current package and displaying a warning triangle when it fails 12 | public extension UIImage { 13 | static func fromPackage(named name: String) -> UIImage { 14 | return .fromPackage(named: name, bundle: .module) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Packages/MinimalOnboarding/Sources/MinimalOnboarding/Screen.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Screen.swift 3 | // 4 | // 5 | // Created by Alex Logan on 30/01/2023. 6 | // 7 | 8 | import Foundation 9 | 10 | public enum Screen { 11 | case intro, signIn, signUp 12 | case emailVerification 13 | case promotion 14 | case phoneNumberEntry, phoneNumberVerification 15 | case phoneConfirmed 16 | case notifications 17 | } 18 | -------------------------------------------------------------------------------- /Packages/MinimalOnboarding/Sources/MinimalOnboarding/Utilities/Binding+SideEffect.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Binding+SideEffect.swift 3 | // 4 | // 5 | // Created by Alex Logan on 30/01/2023. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | extension Binding { 12 | func sideEffect( 13 | onSet: @escaping ((Value, Transaction) -> Void) 14 | ) -> Binding { 15 | .init( 16 | get: { self.wrappedValue }, 17 | set: { newValue, transaction in 18 | onSet(newValue, transaction) 19 | self.transaction(transaction).wrappedValue = newValue 20 | } 21 | ) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Packages/MinimalOnboarding/Sources/MinimalOnboarding/Utilities/OnboardingAlert.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OnboardingAlert.swift 3 | // 4 | // 5 | // Created by Alex Logan on 30/01/2023. 6 | // 7 | 8 | struct OnboardingAlert { 9 | let title: String 10 | let body: String 11 | } 12 | -------------------------------------------------------------------------------- /Packages/MinimalOnboarding/Sources/MinimalOnboarding/Utilities/View+Keyboard.swift: -------------------------------------------------------------------------------- 1 | // 2 | // View+Keyboard.swift 3 | // 4 | // 5 | // Created by Alex Logan on 30/01/2023. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | import Combine 11 | 12 | /// Publisher to read keyboard changes. 13 | extension View { 14 | var keyboardPublisher: AnyPublisher { 15 | Publishers.Merge( 16 | NotificationCenter.default 17 | .publisher(for: UIResponder.keyboardWillShowNotification) 18 | .map { _ in true }, 19 | 20 | NotificationCenter.default 21 | .publisher(for: UIResponder.keyboardWillHideNotification) 22 | .map { _ in false } 23 | ) 24 | .eraseToAnyPublisher() 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Packages/MinimalOnboarding/Tests/MinimalOnboardingTests/MinimalOnboardingTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import MinimalOnboarding 3 | 4 | final class MinimalOnboardingTests: XCTestCase { } 5 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | ![Cover](/Github/Cover.png) 2 | 3 | # Onboardingkit 4 | 5 | OnboardingKit is a set of free to use templates you can throw into your apps to help you skip a couple steps on your journey to best-in-class. 6 | 7 | They're a companion to our [Figma File](https://www.figma.com/community/file/1197544767192716804) which contains the design for each of our flows. 8 | 9 | ## About 10 | 11 | Each template is provided as a small Swift package. They depend on `DesignHelpKit`, so you'll need to pull both into your project if you wish to use them. 12 | 13 | A description of each package's architecture can be found inside the readme for the individual package. 14 | 15 | ## Templates 16 | 17 | Templates will be added over time, and represent our take on one of the designs. They're all designed for iPhone, and will require tweaks for the iPad. 18 | 19 | If youre going to drop one in, be sure to go through and switch out the placeholder copy, and you'll be good to go. 20 | 21 | ### MinimalOnboarding 22 | 23 | ![MinimalOnboarding](/Github/MinimalOnboarding.png) 24 | 25 | A stunning welcome flow that leans heavily on clean, simple illustrations. It skips all the boring stuff and gets right to collecting the phone number, performing verification, and getting them into the product. If your product speaks for itself, this is for you. 26 | 27 | --- 28 | 29 | ## Contribution 30 | 31 | Contribution from the community is welcomed and encouraged. If you see something you'd like to improve, or optimise, please fork the repo and open a PR 😊 --------------------------------------------------------------------------------