├── .DS_Store ├── subscriptions.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ ├── xcshareddata │ │ └── IDEWorkspaceChecks.plist │ └── xcuserdata │ │ └── renat.xcuserdatad │ │ └── UserInterfaceState.xcuserstate ├── xcshareddata │ └── xcschemes │ │ └── subscriptions.xcscheme └── xcuserdata │ └── renat.xcuserdatad │ ├── xcdebugger │ └── Breakpoints_v2.xcbkptlist │ └── xcschemes │ └── xcschememanagement.plist └── subscriptions ├── AppDelegate.swift ├── Assets.xcassets ├── AppIcon.appiconset │ └── Contents.json └── Contents.json ├── Base.lproj └── LaunchScreen.storyboard ├── Constants.swift ├── ContentView.swift ├── Extensions.swift ├── IAPManager.swift ├── Info.plist ├── ProductsStore.swift ├── PurchaseButton.swift ├── PurchaseView.swift └── SceneDelegate.swift /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apphud/ios-swiftui-subscriptions/e6b31a961e81340ef71f20568455136c55269133/.DS_Store -------------------------------------------------------------------------------- /subscriptions.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 945654F822BD339D00497E83 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 945654F722BD339D00497E83 /* ContentView.swift */; }; 11 | 945654FA22BE1C1300497E83 /* ProductsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 945654F922BE1C1300497E83 /* ProductsStore.swift */; }; 12 | 94671EFC22B7C523002E0E14 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94671EFB22B7C523002E0E14 /* AppDelegate.swift */; }; 13 | 94671EFE22B7C523002E0E14 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94671EFD22B7C523002E0E14 /* SceneDelegate.swift */; }; 14 | 94671F0522B7C525002E0E14 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 94671F0422B7C525002E0E14 /* Assets.xcassets */; }; 15 | 94671F0822B7C525002E0E14 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 94671F0622B7C525002E0E14 /* LaunchScreen.storyboard */; }; 16 | 94671F1022B7C545002E0E14 /* IAPManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94671F0F22B7C545002E0E14 /* IAPManager.swift */; }; 17 | 947321FC22BD1AAB007BDB36 /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 947321FB22BD1AAB007BDB36 /* Extensions.swift */; }; 18 | 94D0E0F922BCC6320041BF91 /* PurchaseView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94D0E0F822BCC6320041BF91 /* PurchaseView.swift */; }; 19 | 94D0E0FB22BCCDE50041BF91 /* PurchaseButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94D0E0FA22BCCDE50041BF91 /* PurchaseButton.swift */; }; 20 | 94D98DF022BE26CC004911BF /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94D98DEF22BE26CC004911BF /* Constants.swift */; }; 21 | /* End PBXBuildFile section */ 22 | 23 | /* Begin PBXFileReference section */ 24 | 945654F722BD339D00497E83 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 25 | 945654F922BE1C1300497E83 /* ProductsStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductsStore.swift; sourceTree = ""; }; 26 | 94671EF822B7C523002E0E14 /* subscriptions.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = subscriptions.app; sourceTree = BUILT_PRODUCTS_DIR; }; 27 | 94671EFB22B7C523002E0E14 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 28 | 94671EFD22B7C523002E0E14 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 29 | 94671F0422B7C525002E0E14 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 30 | 94671F0722B7C525002E0E14 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 31 | 94671F0922B7C525002E0E14 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 32 | 94671F0F22B7C545002E0E14 /* IAPManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IAPManager.swift; sourceTree = ""; }; 33 | 947321FB22BD1AAB007BDB36 /* Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = ""; }; 34 | 94D0E0F822BCC6320041BF91 /* PurchaseView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurchaseView.swift; sourceTree = ""; }; 35 | 94D0E0FA22BCCDE50041BF91 /* PurchaseButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurchaseButton.swift; sourceTree = ""; }; 36 | 94D98DEF22BE26CC004911BF /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; 37 | /* End PBXFileReference section */ 38 | 39 | /* Begin PBXFrameworksBuildPhase section */ 40 | 94671EF522B7C523002E0E14 /* Frameworks */ = { 41 | isa = PBXFrameworksBuildPhase; 42 | buildActionMask = 2147483647; 43 | files = ( 44 | ); 45 | runOnlyForDeploymentPostprocessing = 0; 46 | }; 47 | /* End PBXFrameworksBuildPhase section */ 48 | 49 | /* Begin PBXGroup section */ 50 | 94671EEF22B7C523002E0E14 = { 51 | isa = PBXGroup; 52 | children = ( 53 | 94671EFA22B7C523002E0E14 /* subscriptions */, 54 | 94671EF922B7C523002E0E14 /* Products */, 55 | ); 56 | sourceTree = ""; 57 | }; 58 | 94671EF922B7C523002E0E14 /* Products */ = { 59 | isa = PBXGroup; 60 | children = ( 61 | 94671EF822B7C523002E0E14 /* subscriptions.app */, 62 | ); 63 | name = Products; 64 | sourceTree = ""; 65 | }; 66 | 94671EFA22B7C523002E0E14 /* subscriptions */ = { 67 | isa = PBXGroup; 68 | children = ( 69 | 94671EFB22B7C523002E0E14 /* AppDelegate.swift */, 70 | 94671EFD22B7C523002E0E14 /* SceneDelegate.swift */, 71 | 945654F722BD339D00497E83 /* ContentView.swift */, 72 | 94D98DEF22BE26CC004911BF /* Constants.swift */, 73 | 945654F922BE1C1300497E83 /* ProductsStore.swift */, 74 | 94D0E0F822BCC6320041BF91 /* PurchaseView.swift */, 75 | 94D0E0FA22BCCDE50041BF91 /* PurchaseButton.swift */, 76 | 947321FB22BD1AAB007BDB36 /* Extensions.swift */, 77 | 94671F0F22B7C545002E0E14 /* IAPManager.swift */, 78 | 94671F0422B7C525002E0E14 /* Assets.xcassets */, 79 | 94671F0622B7C525002E0E14 /* LaunchScreen.storyboard */, 80 | 94671F0922B7C525002E0E14 /* Info.plist */, 81 | ); 82 | path = subscriptions; 83 | sourceTree = ""; 84 | }; 85 | /* End PBXGroup section */ 86 | 87 | /* Begin PBXNativeTarget section */ 88 | 94671EF722B7C523002E0E14 /* subscriptions */ = { 89 | isa = PBXNativeTarget; 90 | buildConfigurationList = 94671F0C22B7C525002E0E14 /* Build configuration list for PBXNativeTarget "subscriptions" */; 91 | buildPhases = ( 92 | 94671EF422B7C523002E0E14 /* Sources */, 93 | 94671EF522B7C523002E0E14 /* Frameworks */, 94 | 94671EF622B7C523002E0E14 /* Resources */, 95 | ); 96 | buildRules = ( 97 | ); 98 | dependencies = ( 99 | ); 100 | name = subscriptions; 101 | productName = subscriptions; 102 | productReference = 94671EF822B7C523002E0E14 /* subscriptions.app */; 103 | productType = "com.apple.product-type.application"; 104 | }; 105 | /* End PBXNativeTarget section */ 106 | 107 | /* Begin PBXProject section */ 108 | 94671EF022B7C523002E0E14 /* Project object */ = { 109 | isa = PBXProject; 110 | attributes = { 111 | LastSwiftUpdateCheck = 1100; 112 | LastUpgradeCheck = 1100; 113 | ORGANIZATIONNAME = apphud; 114 | TargetAttributes = { 115 | 94671EF722B7C523002E0E14 = { 116 | CreatedOnToolsVersion = 11.0; 117 | }; 118 | }; 119 | }; 120 | buildConfigurationList = 94671EF322B7C523002E0E14 /* Build configuration list for PBXProject "subscriptions" */; 121 | compatibilityVersion = "Xcode 9.3"; 122 | developmentRegion = en; 123 | hasScannedForEncodings = 0; 124 | knownRegions = ( 125 | en, 126 | Base, 127 | ); 128 | mainGroup = 94671EEF22B7C523002E0E14; 129 | productRefGroup = 94671EF922B7C523002E0E14 /* Products */; 130 | projectDirPath = ""; 131 | projectRoot = ""; 132 | targets = ( 133 | 94671EF722B7C523002E0E14 /* subscriptions */, 134 | ); 135 | }; 136 | /* End PBXProject section */ 137 | 138 | /* Begin PBXResourcesBuildPhase section */ 139 | 94671EF622B7C523002E0E14 /* Resources */ = { 140 | isa = PBXResourcesBuildPhase; 141 | buildActionMask = 2147483647; 142 | files = ( 143 | 94671F0822B7C525002E0E14 /* LaunchScreen.storyboard in Resources */, 144 | 94671F0522B7C525002E0E14 /* Assets.xcassets in Resources */, 145 | ); 146 | runOnlyForDeploymentPostprocessing = 0; 147 | }; 148 | /* End PBXResourcesBuildPhase section */ 149 | 150 | /* Begin PBXSourcesBuildPhase section */ 151 | 94671EF422B7C523002E0E14 /* Sources */ = { 152 | isa = PBXSourcesBuildPhase; 153 | buildActionMask = 2147483647; 154 | files = ( 155 | 94671F1022B7C545002E0E14 /* IAPManager.swift in Sources */, 156 | 947321FC22BD1AAB007BDB36 /* Extensions.swift in Sources */, 157 | 945654F822BD339D00497E83 /* ContentView.swift in Sources */, 158 | 94D0E0F922BCC6320041BF91 /* PurchaseView.swift in Sources */, 159 | 94671EFC22B7C523002E0E14 /* AppDelegate.swift in Sources */, 160 | 94D98DF022BE26CC004911BF /* Constants.swift in Sources */, 161 | 945654FA22BE1C1300497E83 /* ProductsStore.swift in Sources */, 162 | 94D0E0FB22BCCDE50041BF91 /* PurchaseButton.swift in Sources */, 163 | 94671EFE22B7C523002E0E14 /* SceneDelegate.swift in Sources */, 164 | ); 165 | runOnlyForDeploymentPostprocessing = 0; 166 | }; 167 | /* End PBXSourcesBuildPhase section */ 168 | 169 | /* Begin PBXVariantGroup section */ 170 | 94671F0622B7C525002E0E14 /* LaunchScreen.storyboard */ = { 171 | isa = PBXVariantGroup; 172 | children = ( 173 | 94671F0722B7C525002E0E14 /* Base */, 174 | ); 175 | name = LaunchScreen.storyboard; 176 | sourceTree = ""; 177 | }; 178 | /* End PBXVariantGroup section */ 179 | 180 | /* Begin XCBuildConfiguration section */ 181 | 94671F0A22B7C525002E0E14 /* Debug */ = { 182 | isa = XCBuildConfiguration; 183 | buildSettings = { 184 | ALWAYS_SEARCH_USER_PATHS = NO; 185 | CLANG_ANALYZER_NONNULL = YES; 186 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 187 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 188 | CLANG_CXX_LIBRARY = "libc++"; 189 | CLANG_ENABLE_MODULES = YES; 190 | CLANG_ENABLE_OBJC_ARC = YES; 191 | CLANG_ENABLE_OBJC_WEAK = YES; 192 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 193 | CLANG_WARN_BOOL_CONVERSION = YES; 194 | CLANG_WARN_COMMA = YES; 195 | CLANG_WARN_CONSTANT_CONVERSION = YES; 196 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 197 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 198 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 199 | CLANG_WARN_EMPTY_BODY = YES; 200 | CLANG_WARN_ENUM_CONVERSION = YES; 201 | CLANG_WARN_INFINITE_RECURSION = YES; 202 | CLANG_WARN_INT_CONVERSION = YES; 203 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 204 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 205 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 206 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 207 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 208 | CLANG_WARN_STRICT_PROTOTYPES = YES; 209 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 210 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 211 | CLANG_WARN_UNREACHABLE_CODE = YES; 212 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 213 | COPY_PHASE_STRIP = NO; 214 | DEBUG_INFORMATION_FORMAT = dwarf; 215 | ENABLE_STRICT_OBJC_MSGSEND = YES; 216 | ENABLE_TESTABILITY = YES; 217 | GCC_C_LANGUAGE_STANDARD = gnu11; 218 | GCC_DYNAMIC_NO_PIC = NO; 219 | GCC_NO_COMMON_BLOCKS = YES; 220 | GCC_OPTIMIZATION_LEVEL = 0; 221 | GCC_PREPROCESSOR_DEFINITIONS = ( 222 | "DEBUG=1", 223 | "$(inherited)", 224 | ); 225 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 226 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 227 | GCC_WARN_UNDECLARED_SELECTOR = YES; 228 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 229 | GCC_WARN_UNUSED_FUNCTION = YES; 230 | GCC_WARN_UNUSED_VARIABLE = YES; 231 | IPHONEOS_DEPLOYMENT_TARGET = 13.0; 232 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 233 | MTL_FAST_MATH = YES; 234 | ONLY_ACTIVE_ARCH = YES; 235 | SDKROOT = iphoneos; 236 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 237 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 238 | }; 239 | name = Debug; 240 | }; 241 | 94671F0B22B7C525002E0E14 /* Release */ = { 242 | isa = XCBuildConfiguration; 243 | buildSettings = { 244 | ALWAYS_SEARCH_USER_PATHS = NO; 245 | CLANG_ANALYZER_NONNULL = YES; 246 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 247 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 248 | CLANG_CXX_LIBRARY = "libc++"; 249 | CLANG_ENABLE_MODULES = YES; 250 | CLANG_ENABLE_OBJC_ARC = YES; 251 | CLANG_ENABLE_OBJC_WEAK = YES; 252 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 253 | CLANG_WARN_BOOL_CONVERSION = YES; 254 | CLANG_WARN_COMMA = YES; 255 | CLANG_WARN_CONSTANT_CONVERSION = YES; 256 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 257 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 258 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 259 | CLANG_WARN_EMPTY_BODY = YES; 260 | CLANG_WARN_ENUM_CONVERSION = YES; 261 | CLANG_WARN_INFINITE_RECURSION = YES; 262 | CLANG_WARN_INT_CONVERSION = YES; 263 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 264 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 265 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 266 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 267 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 268 | CLANG_WARN_STRICT_PROTOTYPES = YES; 269 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 270 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 271 | CLANG_WARN_UNREACHABLE_CODE = YES; 272 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 273 | COPY_PHASE_STRIP = NO; 274 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 275 | ENABLE_NS_ASSERTIONS = NO; 276 | ENABLE_STRICT_OBJC_MSGSEND = YES; 277 | GCC_C_LANGUAGE_STANDARD = gnu11; 278 | GCC_NO_COMMON_BLOCKS = YES; 279 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 280 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 281 | GCC_WARN_UNDECLARED_SELECTOR = YES; 282 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 283 | GCC_WARN_UNUSED_FUNCTION = YES; 284 | GCC_WARN_UNUSED_VARIABLE = YES; 285 | IPHONEOS_DEPLOYMENT_TARGET = 13.0; 286 | MTL_ENABLE_DEBUG_INFO = NO; 287 | MTL_FAST_MATH = YES; 288 | SDKROOT = iphoneos; 289 | SWIFT_COMPILATION_MODE = wholemodule; 290 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 291 | VALIDATE_PRODUCT = YES; 292 | }; 293 | name = Release; 294 | }; 295 | 94671F0D22B7C525002E0E14 /* Debug */ = { 296 | isa = XCBuildConfiguration; 297 | buildSettings = { 298 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 299 | CODE_SIGN_IDENTITY = "Apple Development"; 300 | CODE_SIGN_STYLE = Manual; 301 | DEVELOPMENT_TEAM = ""; 302 | INFOPLIST_FILE = subscriptions/Info.plist; 303 | IPHONEOS_DEPLOYMENT_TARGET = 13.0; 304 | LD_RUNPATH_SEARCH_PATHS = ( 305 | "$(inherited)", 306 | "@executable_path/Frameworks", 307 | ); 308 | PRODUCT_BUNDLE_IDENTIFIER = com.apphud.subscriptionstest; 309 | PRODUCT_NAME = "$(TARGET_NAME)"; 310 | PROVISIONING_PROFILE_SPECIFIER = ""; 311 | SWIFT_VERSION = 5.0; 312 | TARGETED_DEVICE_FAMILY = "1,2"; 313 | }; 314 | name = Debug; 315 | }; 316 | 94671F0E22B7C525002E0E14 /* Release */ = { 317 | isa = XCBuildConfiguration; 318 | buildSettings = { 319 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 320 | CODE_SIGN_IDENTITY = "Apple Development"; 321 | CODE_SIGN_STYLE = Manual; 322 | DEVELOPMENT_TEAM = ""; 323 | INFOPLIST_FILE = subscriptions/Info.plist; 324 | IPHONEOS_DEPLOYMENT_TARGET = 13.0; 325 | LD_RUNPATH_SEARCH_PATHS = ( 326 | "$(inherited)", 327 | "@executable_path/Frameworks", 328 | ); 329 | PRODUCT_BUNDLE_IDENTIFIER = com.apphud.subscriptionstest; 330 | PRODUCT_NAME = "$(TARGET_NAME)"; 331 | PROVISIONING_PROFILE_SPECIFIER = ""; 332 | SWIFT_VERSION = 5.0; 333 | TARGETED_DEVICE_FAMILY = "1,2"; 334 | }; 335 | name = Release; 336 | }; 337 | /* End XCBuildConfiguration section */ 338 | 339 | /* Begin XCConfigurationList section */ 340 | 94671EF322B7C523002E0E14 /* Build configuration list for PBXProject "subscriptions" */ = { 341 | isa = XCConfigurationList; 342 | buildConfigurations = ( 343 | 94671F0A22B7C525002E0E14 /* Debug */, 344 | 94671F0B22B7C525002E0E14 /* Release */, 345 | ); 346 | defaultConfigurationIsVisible = 0; 347 | defaultConfigurationName = Release; 348 | }; 349 | 94671F0C22B7C525002E0E14 /* Build configuration list for PBXNativeTarget "subscriptions" */ = { 350 | isa = XCConfigurationList; 351 | buildConfigurations = ( 352 | 94671F0D22B7C525002E0E14 /* Debug */, 353 | 94671F0E22B7C525002E0E14 /* Release */, 354 | ); 355 | defaultConfigurationIsVisible = 0; 356 | defaultConfigurationName = Release; 357 | }; 358 | /* End XCConfigurationList section */ 359 | }; 360 | rootObject = 94671EF022B7C523002E0E14 /* Project object */; 361 | } 362 | -------------------------------------------------------------------------------- /subscriptions.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /subscriptions.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /subscriptions.xcodeproj/project.xcworkspace/xcuserdata/renat.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apphud/ios-swiftui-subscriptions/e6b31a961e81340ef71f20568455136c55269133/subscriptions.xcodeproj/project.xcworkspace/xcuserdata/renat.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /subscriptions.xcodeproj/xcshareddata/xcschemes/subscriptions.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 | -------------------------------------------------------------------------------- /subscriptions.xcodeproj/xcuserdata/renat.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | -------------------------------------------------------------------------------- /subscriptions.xcodeproj/xcuserdata/renat.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | subscriptions.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | SuppressBuildableAutocreation 14 | 15 | 94671EF722B7C523002E0E14 16 | 17 | primary 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /subscriptions/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // https://apphud.com 4 | // 5 | // Created by Apphud on 17/06/2019. 6 | // Copyright © 2019 apphud. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | 15 | 16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 17 | // Override point for customization after application launch. 18 | 19 | return true 20 | } 21 | 22 | func applicationWillTerminate(_ application: UIApplication) { 23 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 24 | } 25 | 26 | // MARK: UISceneSession Lifecycle 27 | 28 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 29 | // Called when a new scene session is being created. 30 | // Use this method to select a configuration to create the new scene with. 31 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 32 | } 33 | 34 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { 35 | // Called when the user discards a scene session. 36 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. 37 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. 38 | } 39 | 40 | 41 | } 42 | 43 | -------------------------------------------------------------------------------- /subscriptions/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /subscriptions/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /subscriptions/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /subscriptions/Constants.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Constants.swift 3 | // https://apphud.com 4 | // 5 | // Created by Apphud on 22/06/2019. 6 | // Copyright © 2019 apphud. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | let subscription_1 = "YOUR_PRODUCT_ID" 12 | let subscription_2 = "YOUR_ANOTHER_PRODUCT_ID" 13 | let shared_secret = "YOUR_SHARED_SECRET" 14 | 15 | let terms_text = "Premium subscription is required to get access to all wallpapers. Regardless whether the subscription has free trial period or not it automatically renews with the price and duration given above unless it is canceled at least 24 hours before the end of the current period. Payment will be charged to your Apple ID account at the confirmation of purchase. Your account will be charged for renewal within 24 hours prior to the end of the current period. You can manage and cancel your subscriptions by going to your account settings on the App Store after purchase. Any unused portion of a free trial period, if offered, will be forfeited when the user purchases a subscription to that publication, where applicable." 16 | -------------------------------------------------------------------------------- /subscriptions/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // https://apphud.com 4 | // 5 | // Created by Apphud on 21/06/2019. 6 | // Copyright © 2019 apphud. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftUI 11 | import Combine 12 | import StoreKit 13 | 14 | struct ContentView : View { 15 | 16 | @ObservedObject var productsStore : ProductsStore 17 | @State var show_modal = false 18 | 19 | var body: some View { 20 | 21 | VStack() { 22 | ForEach (productsStore.products, id: \.self) { prod in 23 | Text(prod.subscriptionStatus()).lineLimit(nil).frame(height: 80) 24 | } 25 | Button(action: { 26 | print("Button Pushed") 27 | self.show_modal = true 28 | }) { 29 | Text("Present") 30 | }.sheet(isPresented: self.$show_modal) { 31 | PurchaseView() 32 | } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /subscriptions/Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Extensions.swift 3 | // https://apphud.com 4 | // 5 | // Created by Apphud on 21/06/2019. 6 | // Copyright © 2019 apphud. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import StoreKit 11 | 12 | 13 | 14 | extension SKProduct { 15 | 16 | func subscriptionStatus() -> String { 17 | if let expDate = IAPManager.shared.expirationDateFor(productIdentifier) { 18 | let formatter = DateFormatter() 19 | formatter.dateStyle = .medium 20 | formatter.timeStyle = .medium 21 | 22 | let dateString = formatter.string(from: expDate) 23 | 24 | if Date() > expDate { 25 | return "Subscription expired: \(localizedTitle) at: \(dateString)" 26 | } else { 27 | return "Subscription active: \(localizedTitle) until:\(dateString)" 28 | } 29 | } else { 30 | return "Subscription not purchased: \(localizedTitle)" 31 | } 32 | } 33 | 34 | func localizedPrice() -> String { 35 | let formatter = NumberFormatter() 36 | formatter.numberStyle = .currency 37 | formatter.locale = priceLocale 38 | let text = formatter.string(from: price) 39 | let period = subscriptionPeriod!.unit 40 | var periodString = "" 41 | switch period { 42 | case .day: 43 | periodString = "day" 44 | case .month: 45 | periodString = "month" 46 | case .week: 47 | periodString = "week" 48 | case .year: 49 | periodString = "year" 50 | default: 51 | break 52 | } 53 | let unitCount = subscriptionPeriod!.numberOfUnits 54 | let unitString = unitCount == 1 ? periodString : "\(unitCount) \(periodString)s" 55 | return (text ?? "") + "\nper \(unitString)" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /subscriptions/IAPManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IAPManager.swift 3 | // Apphud 4 | // 5 | // Created by Apphud on 04/01/2019. 6 | // Copyright © 2019 Apphud. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import StoreKit 11 | 12 | public typealias SuccessBlock = () -> Void 13 | public typealias FailureBlock = (Error?) -> Void 14 | public typealias ProductsBlock = ([SKProduct]) -> Void 15 | 16 | let IAP_PRODUCTS_DID_LOAD_NOTIFICATION = Notification.Name("IAP_PRODUCTS_DID_LOAD_NOTIFICATION") 17 | 18 | class IAPManager : NSObject{ 19 | 20 | private var sharedSecret = "" 21 | @objc static let shared = IAPManager() 22 | @objc private(set) var products = [SKProduct]() 23 | 24 | private override init(){} 25 | private var productIds : Set = [] 26 | 27 | private var didLoadsProducts : ProductsBlock? 28 | 29 | private var successBlock : SuccessBlock? 30 | private var failureBlock : FailureBlock? 31 | 32 | private var refreshSubscriptionSuccessBlock : SuccessBlock? 33 | private var refreshSubscriptionFailureBlock : FailureBlock? 34 | 35 | // MARK:- Main methods 36 | 37 | @objc func startWith(arrayOfIds : Set!, sharedSecret : String, callback : @escaping ProductsBlock){ 38 | SKPaymentQueue.default().add(self) 39 | self.didLoadsProducts = callback 40 | self.sharedSecret = sharedSecret 41 | self.productIds = arrayOfIds 42 | loadProducts() 43 | } 44 | 45 | func expirationDateFor(_ identifier : String) -> Date?{ 46 | return UserDefaults.standard.object(forKey: identifier) as? Date 47 | } 48 | 49 | func isActive(product : SKProduct) -> Bool { 50 | if let date = expirationDateFor(product.productIdentifier), Date() < date { 51 | return true 52 | } else { 53 | return false 54 | } 55 | } 56 | 57 | func purchaseProduct(product : SKProduct, success: @escaping SuccessBlock, failure: @escaping FailureBlock){ 58 | 59 | guard SKPaymentQueue.canMakePayments() else { 60 | return 61 | } 62 | guard SKPaymentQueue.default().transactions.last?.transactionState != .purchasing else { 63 | return 64 | } 65 | self.successBlock = success 66 | self.failureBlock = failure 67 | let payment = SKPayment(product: product) 68 | SKPaymentQueue.default().add(payment) 69 | } 70 | 71 | func restorePurchases(success: @escaping SuccessBlock, failure: @escaping FailureBlock){ 72 | self.successBlock = success 73 | self.failureBlock = failure 74 | SKPaymentQueue.default().restoreCompletedTransactions() 75 | } 76 | 77 | /* It's the most simple way to send verify receipt request. Consider this code as for learning purposes. You shouldn't use current code in production apps. 78 | This code doesn't handle errors. 79 | */ 80 | func refreshSubscriptionsStatus(callback : @escaping SuccessBlock, failure : @escaping FailureBlock){ 81 | 82 | self.refreshSubscriptionSuccessBlock = callback 83 | self.refreshSubscriptionFailureBlock = failure 84 | 85 | guard let receiptUrl = Bundle.main.appStoreReceiptURL else { 86 | refreshReceipt() 87 | // do not call block in this case. It will be called inside after receipt refreshing finishes. 88 | return 89 | } 90 | 91 | #if DEBUG 92 | let urlString = "https://sandbox.itunes.apple.com/verifyReceipt" 93 | #else 94 | let urlString = "https://buy.itunes.apple.com/verifyReceipt" 95 | #endif 96 | let receiptData = try? Data(contentsOf: receiptUrl).base64EncodedString() 97 | let requestData = ["receipt-data" : receiptData ?? "", "password" : self.sharedSecret, "exclude-old-transactions" : true] as [String : Any] 98 | var request = URLRequest(url: URL(string: urlString)!) 99 | request.httpMethod = "POST" 100 | request.setValue("Application/json", forHTTPHeaderField: "Content-Type") 101 | let httpBody = try? JSONSerialization.data(withJSONObject: requestData, options: []) 102 | request.httpBody = httpBody 103 | 104 | URLSession.shared.dataTask(with: request) { (data, response, error) in 105 | DispatchQueue.main.async { 106 | if data != nil { 107 | if let json = try? JSONSerialization.jsonObject(with: data!, options: .allowFragments){ 108 | self.parseReceipt(json as! Dictionary) 109 | return 110 | } 111 | } else { 112 | print("error validating receipt: \(error?.localizedDescription ?? "")") 113 | } 114 | self.refreshSubscriptionFailureBlock?(error) 115 | self.cleanUpRefeshReceiptBlocks() 116 | } 117 | }.resume() 118 | } 119 | 120 | /* It's the most simple way to get latest expiration date. Consider this code as for learning purposes. You shouldn't use current code in production apps. 121 | This code doesn't handle errors or some situations like cancellation date. 122 | */ 123 | private func parseReceipt(_ json : Dictionary) { 124 | guard let receipts_array = json["latest_receipt_info"] as? [Dictionary] else { 125 | self.refreshSubscriptionFailureBlock?(nil) 126 | self.cleanUpRefeshReceiptBlocks() 127 | return 128 | } 129 | for receipt in receipts_array { 130 | let productID = receipt["product_id"] as! String 131 | let formatter = DateFormatter() 132 | formatter.dateFormat = "yyyy-MM-dd HH:mm:ss VV" 133 | if let date = formatter.date(from: receipt["expires_date"] as! String) { 134 | if date > Date() || UserDefaults.standard.object(forKey: productID) == nil{ 135 | // do not save expired date to user defaults to avoid overwriting with expired date 136 | UserDefaults.standard.set(date, forKey: productID) 137 | } 138 | } 139 | } 140 | self.refreshSubscriptionSuccessBlock?() 141 | self.cleanUpRefeshReceiptBlocks() 142 | } 143 | 144 | /* 145 | Private method. Should not be called directly. Call refreshSubscriptionsStatus instead. 146 | */ 147 | private func refreshReceipt(){ 148 | let request = SKReceiptRefreshRequest(receiptProperties: nil) 149 | request.delegate = self 150 | request.start() 151 | } 152 | 153 | private func loadProducts(){ 154 | let request = SKProductsRequest.init(productIdentifiers: productIds) 155 | request.delegate = self 156 | request.start() 157 | } 158 | 159 | private func cleanUpRefeshReceiptBlocks(){ 160 | self.refreshSubscriptionSuccessBlock = nil 161 | self.refreshSubscriptionFailureBlock = nil 162 | } 163 | } 164 | 165 | // MARK:- SKReceipt Refresh Request Delegate 166 | 167 | extension IAPManager : SKRequestDelegate { 168 | 169 | func requestDidFinish(_ request: SKRequest) { 170 | if request is SKReceiptRefreshRequest { 171 | refreshSubscriptionsStatus(callback: self.successBlock ?? {}, failure: self.failureBlock ?? {_ in}) 172 | } 173 | } 174 | 175 | func request(_ request: SKRequest, didFailWithError error: Error){ 176 | if request is SKReceiptRefreshRequest { 177 | self.refreshSubscriptionFailureBlock?(error) 178 | self.cleanUpRefeshReceiptBlocks() 179 | } 180 | print("error: \(error.localizedDescription)") 181 | } 182 | } 183 | 184 | // MARK:- SKProducts Request Delegate 185 | 186 | extension IAPManager: SKProductsRequestDelegate { 187 | 188 | public func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) { 189 | products = response.products 190 | DispatchQueue.main.async { 191 | NotificationCenter.default.post(name: IAP_PRODUCTS_DID_LOAD_NOTIFICATION, object: nil) 192 | if response.products.count > 0 { 193 | self.didLoadsProducts?(self.products) 194 | self.didLoadsProducts = nil 195 | } 196 | 197 | } 198 | } 199 | } 200 | 201 | // MARK:- SKPayment Transaction Observer 202 | 203 | extension IAPManager: SKPaymentTransactionObserver { 204 | 205 | public func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) { 206 | 207 | for transaction in transactions { 208 | switch (transaction.transactionState) { 209 | case .purchased: 210 | SKPaymentQueue.default().finishTransaction(transaction) 211 | notifyIsPurchased(transaction: transaction) 212 | break 213 | case .failed: 214 | SKPaymentQueue.default().finishTransaction(transaction) 215 | print("purchase error : \(transaction.error?.localizedDescription ?? "")") 216 | self.failureBlock?(transaction.error) 217 | cleanUp() 218 | break 219 | case .restored: 220 | SKPaymentQueue.default().finishTransaction(transaction) 221 | notifyIsPurchased(transaction: transaction) 222 | break 223 | case .deferred, .purchasing: 224 | break 225 | default: 226 | break 227 | } 228 | } 229 | } 230 | 231 | private func notifyIsPurchased(transaction: SKPaymentTransaction) { 232 | refreshSubscriptionsStatus(callback: { 233 | self.successBlock?() 234 | self.cleanUp() 235 | }) { (error) in 236 | // couldn't verify receipt 237 | self.failureBlock?(error) 238 | self.cleanUp() 239 | } 240 | } 241 | 242 | func cleanUp(){ 243 | self.successBlock = nil 244 | self.failureBlock = nil 245 | } 246 | } 247 | -------------------------------------------------------------------------------- /subscriptions/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UIApplicationSceneManifest 24 | 25 | UIApplicationSupportsMultipleScenes 26 | 27 | UISceneConfigurations 28 | 29 | UIWindowSceneSessionRoleApplication 30 | 31 | 32 | UILaunchStoryboardName 33 | LaunchScreen 34 | UISceneConfigurationName 35 | Default Configuration 36 | UISceneDelegateClassName 37 | $(PRODUCT_MODULE_NAME).SceneDelegate 38 | 39 | 40 | 41 | 42 | UILaunchStoryboardName 43 | LaunchScreen 44 | UIRequiredDeviceCapabilities 45 | 46 | armv7 47 | 48 | UISupportedInterfaceOrientations 49 | 50 | UIInterfaceOrientationPortrait 51 | UIInterfaceOrientationLandscapeLeft 52 | UIInterfaceOrientationLandscapeRight 53 | 54 | UISupportedInterfaceOrientations~ipad 55 | 56 | UIInterfaceOrientationPortrait 57 | UIInterfaceOrientationPortraitUpsideDown 58 | UIInterfaceOrientationLandscapeLeft 59 | UIInterfaceOrientationLandscapeRight 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /subscriptions/ProductsStore.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProductsStore.swift 3 | // https://apphud.com 4 | // 5 | // Created by Apphud on 22/06/2019. 6 | // Copyright © 2019 apphud. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftUI 11 | import Combine 12 | import StoreKit 13 | 14 | class ProductsStore : ObservableObject { 15 | 16 | static let shared = ProductsStore() 17 | 18 | @Published var products: [SKProduct] = [] 19 | @Published var anyString = "123" // little trick to force reload ContentView from PurchaseView by just changing any Published value 20 | 21 | func handleUpdateStore(){ 22 | anyString = UUID().uuidString 23 | } 24 | 25 | func initializeProducts(){ 26 | IAPManager.shared.startWith(arrayOfIds: [subscription_1, subscription_2], sharedSecret: shared_secret) { products in 27 | self.products = products 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /subscriptions/PurchaseButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PurchaseButton.swift 3 | // https://apphud.com 4 | // 5 | // Created by Apphud on 21/06/2019. 6 | // Copyright © 2019 apphud. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftUI 11 | import StoreKit 12 | 13 | struct PurchaseButton : View { 14 | 15 | var block : SuccessBlock! 16 | var product : SKProduct! 17 | 18 | var body: some View { 19 | 20 | Button(action: { 21 | self.block() 22 | }) { 23 | Text(product.localizedPrice()).lineLimit(nil).multilineTextAlignment(.center).font(.subheadline) 24 | }.padding().frame(height: 50).scaledToFill().border(Color.blue, width: 1) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /subscriptions/PurchaseView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // https://apphud.com 4 | // 5 | // Created by Apphud on 21/06/2019. 6 | // Copyright © 2019 apphud. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftUI 11 | import StoreKit 12 | import Combine 13 | 14 | struct PurchaseView : View { 15 | 16 | @State private var isDisabled : Bool = false 17 | 18 | @Environment(\.presentationMode) var presentationMode 19 | 20 | private func dismiss() { 21 | self.presentationMode.wrappedValue.dismiss() 22 | } 23 | 24 | var body: some View { 25 | 26 | ScrollView (showsIndicators: false) { 27 | 28 | VStack { 29 | Text("Get Premium Membership").font(.title) 30 | Text("Choose one of the packages above").font(.subheadline) 31 | 32 | self.purchaseButtons() 33 | self.aboutText() 34 | self.helperButtons() 35 | self.termsText().frame(width: UIScreen.main.bounds.size.width) 36 | self.dismissButton() 37 | 38 | }.frame(width : UIScreen.main.bounds.size.width) 39 | }.disabled(self.isDisabled) 40 | } 41 | 42 | // MARK:- View creations 43 | 44 | func purchaseButtons() -> some View { 45 | // remake to ScrollView if has more than 2 products because they won't fit on screen. 46 | HStack { 47 | Spacer() 48 | ForEach(ProductsStore.shared.products, id: \.self) { prod in 49 | PurchaseButton(block: { 50 | self.purchaseProduct(skproduct: prod) 51 | }, product: prod).disabled(IAPManager.shared.isActive(product: prod)) 52 | } 53 | Spacer() 54 | } 55 | } 56 | 57 | func helperButtons() -> some View{ 58 | HStack { 59 | Button(action: { 60 | self.termsTapped() 61 | }) { 62 | Text("Terms of use").font(.footnote) 63 | } 64 | Button(action: { 65 | self.restorePurchases() 66 | }) { 67 | Text("Restore purchases").font(.footnote) 68 | } 69 | Button(action: { 70 | self.privacyTapped() 71 | }) { 72 | Text("Privacy policy").font(.footnote) 73 | } 74 | }.padding() 75 | } 76 | 77 | func aboutText() -> some View { 78 | Text(""" 79 | • Unlimited searches 80 | • 100GB downloads 81 | • Multiplatform service 82 | """).font(.subheadline).lineLimit(nil) 83 | } 84 | 85 | func termsText() -> some View{ 86 | // Set height to 600 because SwiftUI has bug that multiline text is getting cut even if linelimit is nil. 87 | VStack { 88 | Text(terms_text).font(.footnote).lineLimit(nil).padding() 89 | Spacer() 90 | }.frame(height: 350) 91 | } 92 | 93 | func dismissButton() -> some View { 94 | Button(action: { 95 | self.dismiss() 96 | }) { 97 | Text("Not now").font(.footnote) 98 | }.padding() 99 | } 100 | 101 | //MARK:- Actions 102 | 103 | func restorePurchases(){ 104 | 105 | IAPManager.shared.restorePurchases(success: { 106 | self.isDisabled = false 107 | ProductsStore.shared.handleUpdateStore() 108 | self.dismiss() 109 | 110 | }) { (error) in 111 | self.isDisabled = false 112 | ProductsStore.shared.handleUpdateStore() 113 | 114 | } 115 | } 116 | 117 | func termsTapped(){ 118 | 119 | } 120 | 121 | func privacyTapped(){ 122 | 123 | } 124 | 125 | func purchaseProduct(skproduct : SKProduct){ 126 | print("did tap purchase product: \(skproduct.productIdentifier)") 127 | isDisabled = true 128 | IAPManager.shared.purchaseProduct(product: skproduct, success: { 129 | self.isDisabled = false 130 | ProductsStore.shared.handleUpdateStore() 131 | self.dismiss() 132 | }) { (error) in 133 | self.isDisabled = false 134 | ProductsStore.shared.handleUpdateStore() 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /subscriptions/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // https://apphud.com 4 | // 5 | // Created by Apphud on 17/06/2019. 6 | // Copyright © 2019 apphud. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import SwiftUI 11 | 12 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 17 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. 18 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. 19 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). 20 | ProductsStore.shared.initializeProducts() 21 | 22 | // Use a UIHostingController as window root view controller. 23 | if let windowScene = scene as? UIWindowScene { 24 | let window = UIWindow(windowScene: windowScene) 25 | window.rootViewController = UIHostingController(rootView: ContentView(productsStore: ProductsStore.shared)) 26 | self.window = window 27 | window.makeKeyAndVisible() 28 | } 29 | } 30 | 31 | func sceneDidDisconnect(_ scene: UIScene) { 32 | // Called as the scene is being released by the system. 33 | // This occurs shortly after the scene enters the background, or when its session is discarded. 34 | // Release any resources associated with this scene that can be re-created the next time the scene connects. 35 | // The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead). 36 | } 37 | 38 | func sceneDidBecomeActive(_ scene: UIScene) { 39 | // Called when the scene has moved from an inactive state to an active state. 40 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. 41 | } 42 | 43 | func sceneWillResignActive(_ scene: UIScene) { 44 | // Called when the scene will move from an active state to an inactive state. 45 | // This may occur due to temporary interruptions (ex. an incoming phone call). 46 | } 47 | 48 | func sceneWillEnterForeground(_ scene: UIScene) { 49 | // Called as the scene transitions from the background to the foreground. 50 | // Use this method to undo the changes made on entering the background. 51 | } 52 | 53 | func sceneDidEnterBackground(_ scene: UIScene) { 54 | // Called as the scene transitions from the foreground to the background. 55 | // Use this method to save data, release shared resources, and store enough scene-specific state information 56 | // to restore the scene back to its current state. 57 | } 58 | 59 | 60 | } 61 | 62 | --------------------------------------------------------------------------------