diff --git a/CHANGELOG.md b/CHANGELOG.md index 8eb8cb0aa..6c73a4163 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,28 @@ # CHANGELOG -The changelog for `SuperwallKit`. Also see the [releases](https://github.com/superwall-me/SuperwallKit-iOS/releases) on GitHub. +The changelog for `SuperwallKit`. Also see the [releases](https://github.com/superwall-me/Superwall-iOS/releases) on GitHub. + +## 3.0.0-beta.4 + +### Breaking Changes + +- Moves back to using `Superwall.shared.identify(userId: userId)` and `reset()` instead of logIn/createAccount/logout/reset. This is so that it's easier for integration. However, you can now pass an `IdentityOptions` object to `identify(userId:options)`. This should only be used in advanced use cases. By setting the `restorePaywallAssignments` property of `IdentityOptions` to `true`, it prevents paywalls from showing until after paywall assignments have been restored. If you expect users of your app to switch accounts or delete/reinstall a lot, you'd set this when identifying an existing account. + +### Enhancements + +- Adds `hasActiveSubscriptionDidChange(to:)` delegate function. If you're letting Superwall handle subscription logic you can use this to receive a callback whenever the user's internal subscription status changes. You can also listen to the published `hasActiveSubscription` variable. +- Adds a completion handler to `Superwall.configure(...)` that lets you know when Superwall has finished configuring. You can also listen to the published `isConfigured` variable. +- If you let Superwall handle your subscription-related logic, we now assume that a non-consumable product on your paywall is a lifetime subscription. If not, you'll need to return a `SubscriptionController` from the delegate. +- `handleDeepLink(_:)` now returns a discardable `Bool` indicating whether the deep link was handled. If you're using `application(_:open:options:)` you can return its value there. +- Adds `togglePaywallSpinner(isHidden:)` to arbitrarily toggle the loading spinner on and off. This is particularly useful when you're doing async work when performing a custom action in `handleCustomPaywallAction(withName:)`. + +### Fixes + +- Fixes occasional thread safety related crash when loading products. +- Reverts a issue from the last beta where the paywall spinner would move up before the payment sheet appeared. ## 3.0.0-beta.3 + ### Fixes - Fixes potential crash due to a using a lazy variable. diff --git a/Examples/SwiftUI/README.md b/Examples/SwiftUI/README.md index 9ca9de6d3..3c9281cea 100644 --- a/Examples/SwiftUI/README.md +++ b/Examples/SwiftUI/README.md @@ -8,11 +8,11 @@ Usually, to integrate SuperwallKit into your app, you first need to have configu Feature | Sample Project Location --- | --- -๐Ÿ•น Configuring SuperwallKit | [Services/SuperwallService.swift](Superwall-SwiftUI/Services/SuperwallService.swift#L23) +๐Ÿ•น Configuring SuperwallKit | [Services/SuperwallService.swift](Superwall-SwiftUI/Services/SuperwallService.swift#L31) ๐Ÿ‘ฅ Implementing the delegate | [Services/SuperwallService.swift](Superwall-SwiftUI/Services/SuperwallService.swift#L67) -๐Ÿ‘‰ Tracking an event | [TrackEventModel.swift](Superwall-SwiftUI/TrackEventModel.swift#L46) -๐Ÿ‘ฅ Logging In | [Services/SuperwallService.swift](Superwall-SwiftUI/Services/SuperwallService.swift#L32) -๐Ÿ‘ฅ Logging Out | [Services/SuperwallService.swift](Superwall-SwiftUI/Services/SuperwallService.swift#L47) +๐Ÿ‘‰ Tracking an event | [TrackEventModel.swift](Superwall-SwiftUI/TrackEventModel.swift#L15) +๐Ÿ‘ฅ Identifying account | [Services/SuperwallService.swift](Superwall-SwiftUI/Services/SuperwallService.swift#L31) +๐Ÿ‘ฅ Resetting account | [Services/SuperwallService.swift](Superwall-SwiftUI/Services/SuperwallService.swift#L54) ## Requirements @@ -31,9 +31,9 @@ Otherwise, you can download it from [https://github.com/realm/SwiftLint](https:/ ## Getting Started -Clone or download the SuperwallKit from the [project home page](https://github.com/superwall-me/paywall-ios). Then, open **Superwall-SwiftUI.xcodeproj** in Xcode and take a look at the code inside the [Superwall-SwiftUI](Superwall-SwiftUI) folder. +Clone or download the SuperwallKit from the [project home page](https://github.com/superwall-me/Superwall-iOS). Then, open **Superwall-SwiftUI.xcodeproj** in Xcode and take a look at the code inside the [Superwall-SwiftUI](Superwall-SwiftUI) folder. -Inside the [Services](Superwall-SwiftUI/Services) folder, you'll see some helper classes. [SuperwallService.swift](Superwall-SwiftUI/Services/SuperwallService.swift) handles the setup and delegate methods of the SDK, and [StoreKitService.swift](Superwall-SwiftUI/Services/StoreKitService.swift) handles the purchasing of in-app subscriptions. +Inside the [Services](Superwall-SwiftUI/Services) folder, you'll see some helper classes. [SuperwallService.swift](Superwall-SwiftUI/Services/SuperwallService.swift) handles the setup and delegate methods of the SDK. All subscription-related logic is handled by the SDK but we have included a (commented out) example of how you might implement purchases yourself using StoreKit in [StoreKitService.swift](Superwall-SwiftUI/Services/StoreKitService.swift). [Superwall_SwiftUI-Products.storekit](Superwall-SwiftUI/Superwall_SwiftUI-Products.storekit) is a StoreKit configuration file that is used to mimic the setup of real products on App Store Connect. This is so you can make test purchases within the sample app without having to set up App Store Connect. In a production app, you will need real products configured in App Store Connect but you can also use a StoreKit configuration file for testing purposes if you wish. @@ -45,7 +45,7 @@ Build and run the app and you'll see the welcome screen: The welcome screen

-SuperwallKit is [configured](Superwall-SwiftUI/Services/SuperwallService.swift#L22) on app launch, setting an `apiKey` and `delegate`. +SuperwallKit is [configured](Superwall-SwiftUI/Services/SuperwallService.swift#L31) on app launch, setting an `apiKey` and `delegate`. The SDK sends back events received from the paywall via the delegate methods in [SuperwallService.swift](Superwall-SwiftUI/Services/SuperwallService.swift#L67). You use these methods to make and restore purchases, react to analytical events, as well as tell the SDK whether the user has an active subscription. @@ -53,7 +53,7 @@ The SDK sends back events received from the paywall via the delegate methods in On the welcome screen, enter your name in the **text field**. This saves to the Superwall user attributes using [Superwall.shared.setUserAttributes(_:)](Superwall-SwiftUI/Services/SuperwallService.swift#L62). You don't need to set user attributes, but it can be useful if you want to create a rule to present a paywall based on a specific attribute you've set. You can also recall user attributes on your paywall to personalise the messaging. -Tap **Log In**. This logs the user in to Superwall (with a hardcoded userId that we've set), retrieving any paywalls that have already been assigned to them. If you were to create a new account you'd use `Superwall.shared.createAccount(userId:)` instead. +Tap **Log In**. This identifies the user (with a hardcoded userId that we've set), retrieving any paywalls that have already been assigned to them. You'll see an overview screen: @@ -67,9 +67,9 @@ To present a paywall, you **track** an event. On the [Superwall Dashboard](https://superwall.com/dashboard) you add this event to a Campaign and attach some presentation rules. For this app, we've already done this for you. -When an event is tracked, SuperwallKit evaluates the rules associated with it to determine whether or not to show a paywall. Note that if the delegate method [isUserSubscribed()](Superwall-SwiftUI/SuperwallService.swift#L81) returns `true`, a paywall will not show by default. +When an event is tracked, SuperwallKit evaluates the rules associated with it to determine whether or not to show a paywall. -By calling [Superwall.shared.track(event:params:paywallOverrides:paywallHandler:)](Superwall-SwiftUI/TrackEventModel.swift#L15), you present a paywall in response to the event. For this app, the event is called "MyEvent". +By calling [Superwall.shared.track(event:params:paywallOverrides:paywallHandler:)](Superwall-SwiftUI/TrackEventModel.swift#L15), you present a paywall in response to the event. For this app, the event is called `campaign_trigger`. On screen you'll see some explanatory text and a button that tracks an event: @@ -85,6 +85,6 @@ Tap the **Continue** button in the paywall and "purchase" a subscription. When t ## Support -For an in-depth explanation of how to use the SuperwallKit, you can [view our iOS SDK documentation](https://sdk.superwall.me/documentation/paywall/). If you'd like to view it in Xcode, select **Product โ–ธ Build Documentation**. +For an in-depth explanation of how to use the SuperwallKit, you can [view our iOS SDK documentation](https://sdk.superwall.me/documentation/superwallkit/). If you'd like to view it in Xcode, select **Product โ–ธ Build Documentation**. For general docs that include how to use the Superwall Dashboard, visit [docs.superwall.com](https://docs.superwall.com/docs). diff --git a/Examples/SwiftUI/Superwall-SwiftUI/Services/SuperwallService.swift b/Examples/SwiftUI/Superwall-SwiftUI/Services/SuperwallService.swift index 777507613..aac8e87b0 100644 --- a/Examples/SwiftUI/Superwall-SwiftUI/Services/SuperwallService.swift +++ b/Examples/SwiftUI/Superwall-SwiftUI/Services/SuperwallService.swift @@ -25,40 +25,33 @@ final class SuperwallService { // further down: // Task { - // await StoreKitService.shared.loadSubscriptionState()*/ - Superwall.configure( - apiKey: apiKey, - delegate: shared - ) + // await StoreKitService.shared.loadSubscriptionState() // } + Superwall.configure( + apiKey: apiKey, + delegate: shared + ) + // Getting our logged in status to Superwall. shared.isLoggedIn.send(Superwall.shared.isLoggedIn) } - static func logIn() async { + static func identify() { do { - try await Superwall.shared.logIn(userId: "abc") + try Superwall.shared.identify(userId: "abc") } catch let error as IdentityError { switch error { case .missingUserId: print("The provided userId was empty") - case .alreadyLoggedIn: - print("The user is already logged in") } } catch { print("Unexpected error", error) } } - static func logOut() async { - do { - try await Superwall.shared.logOut() - } catch LogoutError.notLoggedIn { - print("The user is not logged in") - } catch { - print("Unexpected error", error) - } + static func reset() async { + await Superwall.shared.reset() } static func handleDeepLink(_ url: URL) { diff --git a/Examples/SwiftUI/Superwall-SwiftUI/TrackEventModel.swift b/Examples/SwiftUI/Superwall-SwiftUI/TrackEventModel.swift index a965c9ca0..de003c34b 100644 --- a/Examples/SwiftUI/Superwall-SwiftUI/TrackEventModel.swift +++ b/Examples/SwiftUI/Superwall-SwiftUI/TrackEventModel.swift @@ -12,7 +12,7 @@ final class TrackEventModel { // private var cancellable: AnyCancellable? func trackEvent() { - Superwall.shared.track(event: "MyEvent") { paywallState in + Superwall.shared.track(event: "campaign_trigger") { paywallState in switch paywallState { case .presented(let paywallInfo): print("paywall info is", paywallInfo) diff --git a/Examples/SwiftUI/Superwall-SwiftUI/TrackEventView.swift b/Examples/SwiftUI/Superwall-SwiftUI/TrackEventView.swift index f5af26903..45ff0d555 100644 --- a/Examples/SwiftUI/Superwall-SwiftUI/TrackEventView.swift +++ b/Examples/SwiftUI/Superwall-SwiftUI/TrackEventView.swift @@ -23,7 +23,7 @@ struct TrackEventView: View { var body: some View { VStack(spacing: 48) { InfoView( - text: "The button below tracks an event \"MyEvent\".\n\nThis event has been added to a campaign on the Superwall dashboard.\n\nWhen this event is tracked, the rules in the campaign are evaluated.\n\nThe rules match and cause a paywall to show." + text: "The button below tracks an event \"campaign_trigger\".\n\nThis event has been added to a campaign on the Superwall dashboard.\n\nWhen this event is tracked, the rules in the campaign are evaluated.\n\nThe rules match and cause a paywall to show." ) Divider() diff --git a/Examples/UIKit+RevenueCat/README.md b/Examples/UIKit+RevenueCat/README.md index 7310041f3..c962b4a96 100644 --- a/Examples/UIKit+RevenueCat/README.md +++ b/Examples/UIKit+RevenueCat/README.md @@ -8,19 +8,19 @@ Usually, to integrate SuperwallKit into your app, you first need to have configu Feature | Sample Project Location --- | --- -๐Ÿ•น Configuring SuperwallKit and RevenueCat | [PaywallManager.swift](Superwall-UIKit+RevenueCat/PaywallManager.swift#39) -๐Ÿ’ฐ Implementing the Superwall delegate | [PaywallManager.swift](Superwall-UIKit+RevenueCat/PaywallManager.swift#L139) -๐Ÿ˜บ Implementing the RevenueCat delegate | [PaywallManager.swift](Superwall-UIKit+RevenueCat/PaywallManager.swift#L103) -๐Ÿ‘‰ Presenting a paywall | [TrackEventViewController.swift](Superwall-UIKit+RevenueCat/TrackEventViewController.swift#L59) -๐Ÿ‘ฅ Logging In | [PaywallManager.swift](Superwall-UIKit+RevenueCat/PaywallManager.swift#L52) -๐Ÿ‘ฅ Logging Out | [PaywallManager.swift](Superwall-UIKit+RevenueCat/PaywallManager.swift#L73) +๐Ÿ•น Configuring SuperwallKit and RevenueCat | [PaywallManager.swift](Superwall-UIKit+RevenueCat/PaywallManager.swift#37) +๐Ÿ˜บ Implementing the RevenueCat delegate | [PaywallManager.swift](Superwall-UIKit+RevenueCat/PaywallManager.swift#L144) +๐Ÿ’ฐ Implementing the Superwall delegate | [PaywallManager.swift](Superwall-UIKit+RevenueCat/PaywallManager.swift#L170) +๐Ÿ‘‰ Presenting a paywall | [TrackEventViewController.swift](Superwall-UIKit+RevenueCat/TrackEventViewController.swift#L60) +๐Ÿ‘ฅ Logging In | [PaywallManager.swift](Superwall-UIKit+RevenueCat/PaywallManager.swift#L64) +๐Ÿ‘ฅ Logging Out | [PaywallManager.swift](Superwall-UIKit+RevenueCat/PaywallManager.swift#L82) ## Requirements This example app uses: - UIKit -- RevenueCat 4.14.3 +- RevenueCat 4.17.4 - Xcode 14 - iOS 16 - Swift 5.5 @@ -33,7 +33,7 @@ Otherwise, you can download it from [https://github.com/realm/SwiftLint](https:/ ## Getting Started -Clone or download SuperwallKit from the [project home page](https://github.com/superwall-me/paywall-ios). Then, open **Superwall-UIKit+RevenueCat.xcodeproj** in Xcode and take a look at the code inside the [Superwall-UIKit+RevenueCat](Superwall-UIKit+RevenueCat) folder. +Clone or download SuperwallKit from the [project home page](https://github.com/superwall-me/Superwall-iOS). Then, open **Superwall-UIKit+RevenueCat.xcodeproj** in Xcode and take a look at the code inside the [Superwall-UIKit+RevenueCat](Superwall-UIKit+RevenueCat) folder. You'll see a helper file called [PaywallManager.swift](Superwall-UIKit+RevenueCat/PaywallManager.swift) which handles both SuperwallKit and RevenueCat. This includes configuration, delegation, purchasing, restoring and updating and maintaining the user's subscription status. @@ -47,15 +47,15 @@ Build and run the app and you'll see the welcome screen: The welcome screen

-SuperwallKit and RevenueCat are both [configured](Superwall-UIKit+RevenueCat/PaywallManager.swift#L39) on app launch, setting an `apiKey` and `delegate`. +SuperwallKit and RevenueCat are both [configured](Superwall-UIKit+RevenueCat/PaywallManager.swift#L37) on app launch, setting an `apiKey` and `delegate`. -The SDK sends back events received from the paywall via the delegate methods in [PaywallManager.swift](Superwall-UIKit+RevenueCat/PaywallManager.swift#L139). You use these methods to make and restore purchases, react to analytical events, as well as tell Superwall whether the user has an active subscription. +The SDK sends back events received from the paywall via the delegate methods in [PaywallManager.swift](Superwall-UIKit+RevenueCat/PaywallManager.swift#L170). The delegate is responsible for sending back analytical events and providing a `SubscriptionController`. This is a protocol implemented by the PaywallManager that handles all subscription-related logic. ## Logging In -On the welcome screen, enter your name in the **text field**. This saves to the Superwall user attributes using [Superwall.shared.setUserAttributes(_:)](Superwall-UIKit+RevenueCat/PaywallManager.swift#L97). You don't need to set user attributes, but it can be useful if you want to create a rule to present a paywall based on a specific attribute you've set. You can also recall user attributes on your paywall to personalise the messaging. +On the welcome screen, enter your name in the **text field**. This saves to the Superwall user attributes using [Superwall.shared.setUserAttributes(_:)](Superwall-UIKit+RevenueCat/PaywallManager.swift#L102). You don't need to set user attributes, but it can be useful if you want to create a rule to present a paywall based on a specific attribute you've set. You can also recall user attributes on your paywall to personalise the messaging. -Tap **Log In**. This logs the user in to Superwall (with a hardcoded userId that we've set), retrieving any paywalls that have already been assigned to them. If you were to create a new account you'd use `Superwall.shared.createAccount(userId:)` instead. +Tap **Log In**. This identifies the user with Superwall using a hardcoded userId, retrieving any paywalls that have already been assigned to them. It also logs into revenuecat, retrieving the user's subscription status. You'll see an overview screen: @@ -69,9 +69,9 @@ To present a paywall, you **track** an event. On the [Superwall Dashboard](https://superwall.com/dashboard) you add this event to a Campaign and attach some presentation rules. For this app, we've already done this for you. -When an event is tracked, SuperwallKit evaluates the rules associated with it to determine whether or not to show a paywall. Note that if the delegate method [isUserSubscribed()](Superwall-UIKit+RevenueCat/PaywallManager.swift#L163) returns `true`, a paywall will not show by default. +When an event is tracked, SuperwallKit evaluates the rules associated with it to determine whether or not to show a paywall. Note that if the `SubscriptionController` method [isUserSubscribed()](Superwall-UIKit+RevenueCat/PaywallManager.swift#L122) returns `true`, a paywall will not show by default. -By calling [Superwall.shared.track(event:params:paywallOverrides:paywallHandler:)](Superwall-UIKit+RevenueCat/TrackEventViewController.swift#L59), you present a paywall in response to the event. For this app, the event is called "MyEvent". +By calling [Superwall.shared.track(event:params:paywallOverrides:paywallHandler:)](Superwall-UIKit+RevenueCat/TrackEventViewController.swift#L60), you present a paywall in response to the event. For this app, the event is called `campaign_trigger`. On screen you'll see some explanatory text and a button that tracks an event: @@ -83,10 +83,10 @@ Tap the **Track Event** button and you'll see the paywall. If the event is disab ## Purchasing a subscription -Tap the **Continue** button in the paywall and "purchase" a subscription. When the paywall dismisses, try tracking an event. You'll notice the buttons no longer show the paywall. The paywalls are only presented to users who haven't got an active subscription. To cancel the active subscription for an app that's using a storekit configuration file for testing, delete and reinstall the app. +Tap the **Continue** button in the paywall and "purchase" a subscription. When the paywall dismisses, try tracking an event. You'll notice the buttons no longer show the paywall. The paywalls are only presented to users who haven't got an active subscription. To cancel the active subscription for an app that's using a storekit configuration file for testing, delete and reinstall the app. You will need to wait a few minutes until the subscription expires on RevenueCat's side before trying again. ## Support -For an in-depth explanation of how to use SuperwallKit, you can [view our iOS SDK documentation](https://sdk.superwall.me/documentation/paywall/). If you'd like to view it in Xcode, select **Product โ–ธ Build Documentation**. +For an in-depth explanation of how to use SuperwallKit, you can [view our iOS SDK documentation](https://sdk.superwall.me/documentation/superwallkit/). If you'd like to view it in Xcode, select **Product โ–ธ Build Documentation**. For general docs that include how to use the Superwall Dashboard, visit [docs.superwall.com](https://docs.superwall.com/docs). diff --git a/Examples/UIKit+RevenueCat/Superwall-UIKit+RevenueCat/PaywallManager.swift b/Examples/UIKit+RevenueCat/Superwall-UIKit+RevenueCat/PaywallManager.swift index 5f8bf177d..7f47edad6 100644 --- a/Examples/UIKit+RevenueCat/Superwall-UIKit+RevenueCat/PaywallManager.swift +++ b/Examples/UIKit+RevenueCat/Superwall-UIKit+RevenueCat/PaywallManager.swift @@ -45,15 +45,17 @@ final class PaywallManager: NSObject { // configuring Superwall to prevent session_start events from // incorrectly firing due to an incorrect subscription status. Purchases.shared.getCustomerInfo { customerInfo, _ in - if let customerInfo { - shared.updateSubscriptionStatus(using: customerInfo) + DispatchQueue.main.async { + if let customerInfo { + shared.updateSubscriptionStatus(using: customerInfo) + } } - - Superwall.configure( - apiKey: superwallApiKey, - delegate: shared - ) } + + Superwall.configure( + apiKey: superwallApiKey, + delegate: shared + ) } /// Logs the user in to both RevenueCat and Superwall with the specified `userId`. @@ -63,11 +65,9 @@ final class PaywallManager: NSObject { do { let (customerInfo, _) = try await Purchases.shared.logIn(userId) updateSubscriptionStatus(using: customerInfo) - try await Superwall.shared.logIn(userId: userId) + try Superwall.shared.identify(userId: userId) } catch let error as IdentityError { switch error { - case .alreadyLoggedIn: - print("The user is already logged in to Superwall") case .missingUserId: print("The provided userId was empty") } @@ -83,12 +83,7 @@ final class PaywallManager: NSObject { do { let customerInfo = try await Purchases.shared.logOut() updateSubscriptionStatus(using: customerInfo) - try await Superwall.shared.logOut() - } catch let error as LogoutError { - switch error { - case .notLoggedIn: - print("The user was not logged in to Superwall") - } + await Superwall.shared.reset() } catch { print("A RevenueCat error occurred", error) } diff --git a/Examples/UIKit+RevenueCat/Superwall-UIKit+RevenueCat/TrackEventViewController.swift b/Examples/UIKit+RevenueCat/Superwall-UIKit+RevenueCat/TrackEventViewController.swift index a30046f41..ef6f13adc 100644 --- a/Examples/UIKit+RevenueCat/Superwall-UIKit+RevenueCat/TrackEventViewController.swift +++ b/Examples/UIKit+RevenueCat/Superwall-UIKit+RevenueCat/TrackEventViewController.swift @@ -57,7 +57,7 @@ final class TrackEventViewController: UIViewController { } @IBAction private func trackEvent() { - Superwall.shared.track(event: "MyEvent") { paywallState in + Superwall.shared.track(event: "campaign_trigger") { paywallState in switch paywallState { case .presented(let paywallInfo): print("paywall info is", paywallInfo) diff --git a/Examples/UIKit-ObjC/Superwall-UIKit-ObjC.xcodeproj/project.pbxproj b/Examples/UIKit-ObjC/Superwall-UIKit-ObjC.xcodeproj/project.pbxproj index ff227cefb..0f73245e7 100644 --- a/Examples/UIKit-ObjC/Superwall-UIKit-ObjC.xcodeproj/project.pbxproj +++ b/Examples/UIKit-ObjC/Superwall-UIKit-ObjC.xcodeproj/project.pbxproj @@ -419,7 +419,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = CX78747TR3; + DEVELOPMENT_TEAM = ""; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = "Superwall-UIKit-ObjC/Info.plist"; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; @@ -447,7 +447,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = CX78747TR3; + DEVELOPMENT_TEAM = ""; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = "Superwall-UIKit-ObjC/Info.plist"; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; diff --git a/Examples/UIKit-ObjC/Superwall-UIKit-ObjC/SSATrackEventViewController.m b/Examples/UIKit-ObjC/Superwall-UIKit-ObjC/SSATrackEventViewController.m index fd996e0c8..d912b263a 100644 --- a/Examples/UIKit-ObjC/Superwall-UIKit-ObjC/SSATrackEventViewController.m +++ b/Examples/UIKit-ObjC/Superwall-UIKit-ObjC/SSATrackEventViewController.m @@ -47,7 +47,7 @@ - (void)viewWillAppear:(BOOL)animated { - (IBAction)trackEvent:(id)sender { __weak typeof(self) weakSelf = self; - [[Superwall sharedInstance] trackWithEvent:@"MyEvent" + [[Superwall sharedInstance] trackWithEvent:@"campaign_trigger" params:nil products:nil ignoreSubscriptionStatus:NO diff --git a/Examples/UIKit-ObjC/Superwall-UIKit-ObjC/Services/SSASuperwallService.m b/Examples/UIKit-ObjC/Superwall-UIKit-ObjC/Services/SSASuperwallService.m index f2ca76724..2755774bb 100644 --- a/Examples/UIKit-ObjC/Superwall-UIKit-ObjC/Services/SSASuperwallService.m +++ b/Examples/UIKit-ObjC/Superwall-UIKit-ObjC/Services/SSASuperwallService.m @@ -85,15 +85,12 @@ - (void)initialize { [[SSAStoreKitService sharedService] updateSubscribedState]; // Configure Superwall. - [Superwall configureWithApiKey:kDemoAPIKey delegate:self options:nil]; + [Superwall configureWithApiKey:kDemoAPIKey delegate:self options:nil completion:nil]; } - (void)logInWithCompletion:(nullable void (^)(void))completion { - [[Superwall sharedInstance] logInUserId:kDemoUserId completionHandler:^(NSError * _Nullable error) { + [[Superwall sharedInstance] identifyWithUserId:kDemoAPIKey options:nil completionHandler:^(NSError * _Nullable error) { switch (error.code) { - case SWKIdentityErrorAlreadyLoggedIn: - NSLog(@"The user is already logged in"); - break; case SWKIdentityErrorMissingUserId: NSLog(@"The provided userId was empty"); break; @@ -102,31 +99,14 @@ - (void)logInWithCompletion:(nullable void (^)(void))completion { break; } - dispatch_async(dispatch_get_main_queue(), ^{ - if (completion) { - completion(); - } - }); + if (completion) { + completion(); + } }]; } - (void)logOutWithCompletion:(nullable void (^)(void))completion { - [[Superwall sharedInstance] logOutWithCompletionHandler:^(NSError * _Nullable error) { - switch (error.code) { - case SWKLogoutErrorNotLoggedIn: - NSLog(@"The user is not logged in"); - break; - default: - NSLog(@"An unknown error occurred: %@", error.localizedDescription); - break; - } - - dispatch_async(dispatch_get_main_queue(), ^{ - if (completion) { - completion(); - } - }); - }]; + [[Superwall sharedInstance] resetWithCompletionHandler:completion]; } - (void)handleDeepLinkWithURL:(NSURL *)URL { diff --git a/Examples/UIKit-Swift/README.md b/Examples/UIKit-Swift/README.md index ba9647127..1dbc9bfd9 100644 --- a/Examples/UIKit-Swift/README.md +++ b/Examples/UIKit-Swift/README.md @@ -8,10 +8,10 @@ Usually, to integrate SuperwallKit into your app, you first need to have configu Feature | Sample Project Location --- | --- -๐Ÿ•น Configuring SuperwallKit | [Services/SuperwallService.swift](Superwall-UIKit-Swift/Services/SuperwallService.swift#L22) -๐Ÿ‘‰ Presenting a paywall | [ImplicitlyTriggerPaywallViewController.swift](Superwall-UIKit-Swift/TrackEventViewController.swift#L59) -๐Ÿ‘ฅ Logging In | [Services/SuperwallService.swift](Superwall-UIKit-Swift/Services/SuperwallService.swift#L30) -๐Ÿ‘ฅ Logging Out | [Services/SuperwallService.swift](Superwall-UIKit-Swift/Services/SuperwallService.swift#L45) +๐Ÿ•น Configuring SuperwallKit | [Services/SuperwallService.swift](Superwall-UIKit-Swift/Services/SuperwallService.swift#L30) +๐Ÿ‘‰ Presenting a paywall | [TrackEventViewController.swift](Superwall-UIKit-Swift/TrackEventViewController.swift#L59) +๐Ÿ‘ฅ Identifying account | [Services/SuperwallService.swift](Superwall-UIKit-Swift/Services/SuperwallService.swift#L38) +๐Ÿ‘ฅ Resetting account | [Services/SuperwallService.swift](Superwall-UIKit-Swift/Services/SuperwallService.swift#L45) ## Requirements @@ -30,9 +30,9 @@ Otherwise, you can download it from [https://github.com/realm/SwiftLint](https:/ ## Getting Started -Clone or download SuperwallKit from the [project home page](https://github.com/superwall-me/paywall-ios). Then, open **Superwall-UIKit-Swift.xcodeproj** in Xcode and take a look at the code inside the [Superwall-UIKit-Swift]() folder. +Clone or download SuperwallKit from the [project home page](https://github.com/superwall-me/Superwall-iOS). Then, open **Superwall-UIKit-Swift.xcodeproj** in Xcode and take a look at the code inside the [Superwall-UIKit-Swift](Superwall-UIKit-Swift) folder. -Inside the [Services](Superwall-UIKit-Swift/Services) folder, you'll see some helper classes. [SuperwallService.swift](Superwall-UIKit-Swift/Services/SuperwallService.swift) handles the setup and delegate methods of the SDK, and [StoreKitService.swift](Superwall-UIKit-Swift/Services/StoreKitService.swift) handles the purchasing of in-app subscriptions. +Inside the [Services](Superwall-UIKit-Swift/Services) folder, you'll see some helper classes. [SuperwallService.swift](Superwall-UIKit-Swift/Services/SuperwallService.swift) handles the setup and delegate methods of the SDK. All subscription-related logic is handled by the SDK but we have included a (commented out) example of how you might implement purchases yourself using StoreKit in [StoreKitService.swift](Superwall-UIKit-Swift/Services/StoreKitService.swift). [Superwall_UIKit-Swift-Products.storekit](Superwall-UIKit-Swift/Superwall_UIKit-Swift-Products.storekit) is a StoreKit configuration file that is used to mimic the setup of real products on App Store Connect. This is so you can make test purchases within the sample app without having to set up App Store Connect. In a production app, you will need real products configured in App Store Connect but you can also use a StoreKit configuration file for testing purposes if you wish. @@ -44,16 +44,15 @@ Build and run the app and you'll see the welcome screen: The welcome screen

-SuperwallKit is [configured](Superwall-UIKit-Swift/Services/SuperwallService.swift#L20) on app launch, setting an `apiKey` and `delegate`. +SuperwallKit is [configured](Superwall-UIKit-Swift/Services/SuperwallService.swift#L30) on app launch, setting an `apiKey` and `delegate`. -The SDK sends back events received from the paywall via the delegate methods in [SuperwallService.swift](Superwall-UIKit-Swift/Services/SuperwallService.swift). You use these methods to make and restore purchases, react to analytical events, as well as tell the SDK whether the user has an active subscription. +The SDK sends back events received from the paywall via the delegate methods in [SuperwallService.swift](Superwall-UIKit-Swift/Services/SuperwallService.swift#L64). You use these methods to make and restore purchases, react to analytical events, as well as tell the SDK whether the user has an active subscription. ## Logging In -On the welcome screen, enter your name in the **text field**This saves to the Superwall user attributes using [Superwall.shared.setUserAttributes(_:)](Superwall-UIKit-Swift/Services/SuperwallService.swift#L63). You don't need to set user attributes, but it can be useful if you want to create a rule to present a paywall based on a specific attribute you've set. You can also recall user attributes on your paywall to personalise the messaging. - -Tap **Log In**. This logs the user in to Superwall (with a hardcoded userId that we've set), retrieving any paywalls that have already been assigned to them. If you were to create a new account you'd use `Superwall.shared.createAccount(userId:)` instead. +On the welcome screen, enter your name in the **text field**This saves to the Superwall user attributes using [Superwall.shared.setUserAttributes(_:)](Superwall-UIKit-Swift/Services/SuperwallService.swift#L58). You don't need to set user attributes, but it can be useful if you want to create a rule to present a paywall based on a specific attribute you've set. You can also recall user attributes on your paywall to personalise the messaging. +Tap **Log In**. This identifies the user (with a hardcoded userId that we've set), retrieving any paywalls that have already been assigned to them. You'll see an overview screen: @@ -67,9 +66,9 @@ To present a paywall, you **track** an event. On the [Superwall Dashboard](https://superwall.com/dashboard) you add this event to a Campaign and attach some presentation rules. For this app, we've already done this for you. -When an event is tracked, SuperwallKit evaluates the rules associated with it to determine whether or not to show a paywall. Note that if the delegate method [isUserSubscribed()](Superwall-UIKit-Swift/SuperwallService.swift#L82) returns `true`, a paywall will not show by default. +When an event is tracked, SuperwallKit evaluates the rules associated with it to determine whether or not to show a paywall. -By calling [Superwall.shared.track(event:params:paywallOverrides:paywallHandler:)](Superwall-UIKit-Swift/TrackEventViewController.swift#L57), you present a paywall in response to the event. For this app, the event is called "MyEvent". +By calling [Superwall.shared.track(event:params:paywallOverrides:paywallHandler:)](Superwall-UIKit-Swift/TrackEventViewController.swift#L57), you present a paywall in response to the event. For this app, the event is called `campaign_trigger`. On screen you'll see some explanatory text and a button that tracks an event: @@ -85,6 +84,6 @@ Tap the **Continue** button in the paywall and "purchase" a subscription. When t ## Support -For an in-depth explanation of how to use SuperwallKit, you can [view our iOS SDK documentation](https://sdk.superwall.me/documentation/paywall/). If you'd like to view it in Xcode, select **Product โ–ธ Build Documentation**. +For an in-depth explanation of how to use SuperwallKit, you can [view our iOS SDK documentation](https://sdk.superwall.me/documentation/superwallkit/). If you'd like to view it in Xcode, select **Product โ–ธ Build Documentation**. For general docs that include how to use the Superwall Dashboard, visit [docs.superwall.com](https://docs.superwall.com/docs). diff --git a/Examples/UIKit-Swift/Superwall-UIKit-Swift.xcodeproj/project.pbxproj b/Examples/UIKit-Swift/Superwall-UIKit-Swift.xcodeproj/project.pbxproj index bd5c84b47..06932f60b 100644 --- a/Examples/UIKit-Swift/Superwall-UIKit-Swift.xcodeproj/project.pbxproj +++ b/Examples/UIKit-Swift/Superwall-UIKit-Swift.xcodeproj/project.pbxproj @@ -396,7 +396,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = 9N28YS58J3; + DEVELOPMENT_TEAM = ""; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = "Superwall-UIKit-Swift/Info.plist"; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; diff --git a/Examples/UIKit-Swift/Superwall-UIKit-Swift/Services/SuperwallService.swift b/Examples/UIKit-Swift/Superwall-UIKit-Swift/Services/SuperwallService.swift index 8ce209302..5f5b418cb 100644 --- a/Examples/UIKit-Swift/Superwall-UIKit-Swift/Services/SuperwallService.swift +++ b/Examples/UIKit-Swift/Superwall-UIKit-Swift/Services/SuperwallService.swift @@ -24,21 +24,20 @@ final class SuperwallService { // further down: // Task { - // await StoreKitService.shared.loadSubscriptionState()*/ - Superwall.configure( - apiKey: apiKey, - delegate: shared - ) + // await StoreKitService.shared.loadSubscriptionState() // } + + Superwall.configure( + apiKey: apiKey, + delegate: shared + ) } - static func logIn() async { + static func identify() { do { - try await Superwall.shared.logIn(userId: "abc") + try Superwall.shared.identify(userId: "abc") } catch let error as IdentityError { switch error { - case .alreadyLoggedIn: - print("The user is already logged in") case .missingUserId: print("The provided userId was empty") } @@ -47,17 +46,8 @@ final class SuperwallService { } } - static func logOut() async { - do { - try await Superwall.shared.logOut() - } catch let error as LogoutError { - switch error { - case .notLoggedIn: - print("The user is not logged in") - } - } catch { - print("An unknown error occurred", error) - } + static func reset() async { + await Superwall.shared.reset() } static func handleDeepLink(_ url: URL) { @@ -156,6 +146,7 @@ extension SuperwallService: SuperwallDelegate { } } +// MARK: - SubscriptionController extension SuperwallService: SubscriptionController { func purchase(product: SKProduct) async -> PurchaseResult { return await StoreKitService.shared.purchase(product) diff --git a/Examples/UIKit-Swift/Superwall-UIKit-Swift/TrackEventViewController.swift b/Examples/UIKit-Swift/Superwall-UIKit-Swift/TrackEventViewController.swift index 7ff51fd3e..cf446dfed 100644 --- a/Examples/UIKit-Swift/Superwall-UIKit-Swift/TrackEventViewController.swift +++ b/Examples/UIKit-Swift/Superwall-UIKit-Swift/TrackEventViewController.swift @@ -50,14 +50,14 @@ final class TrackEventViewController: UIViewController { @IBAction private func logOut() { UserDefaults.standard.setValue(false, forKey: "IsLoggedIn") Task { - await SuperwallService.logOut() + await SuperwallService.reset() _ = navigationController?.popToRootViewController(animated: true) } } @IBAction private func trackEvent() { Superwall.shared.track( - event: "MyEvent" + event: "campaign_trigger" ) { paywallState in switch paywallState { case .presented(let paywallInfo): diff --git a/Examples/UIKit-Swift/Superwall-UIKit-Swift/WelcomeViewController.swift b/Examples/UIKit-Swift/Superwall-UIKit-Swift/WelcomeViewController.swift index 9f0115f2a..7e0ed2b1b 100644 --- a/Examples/UIKit-Swift/Superwall-UIKit-Swift/WelcomeViewController.swift +++ b/Examples/UIKit-Swift/Superwall-UIKit-Swift/WelcomeViewController.swift @@ -41,7 +41,7 @@ final class WelcomeViewController: UIViewController { if let name = textField.text { SuperwallService.setName(to: name) } - await SuperwallService.logIn() + SuperwallService.identify() next() } } diff --git a/README.md b/README.md index 3f585177b..5898a30c7 100644 --- a/README.md +++ b/README.md @@ -15,14 +15,14 @@ iOS Versions Supported - + MIT License Community Active - Version Number + Version Number

@@ -55,7 +55,7 @@ We're in the process of releasing a new v3 version of the framework called **Sup โœ๏ธ | A/B Testing - automatically calculate metrics for different paywalls ๐Ÿ“ | [Online documentation](https://docs.superwall.com/v3.0/docs) up to date ๐Ÿ”€ | [Integrations](https://docs.superwall.com/v3.0/docs) - over a dozen integrations to easily send conversion data where you need it -๐Ÿ’ฏ | Well maintained - [frequent releases](https://github.com/superwall-me/SuperwallKit-iOS/releases) +๐Ÿ’ฏ | Well maintained - [frequent releases](https://github.com/superwall-me/Superwall-iOS/releases) ๐Ÿ“ฎ | Great support - email a founder: jake@superwall.com ## Installation @@ -65,8 +65,8 @@ We're in the process of releasing a new v3 version of the framework called **Sup The preferred installation method is with [Swift Package Manager](https://swift.org/package-manager/). This is a tool for automating the distribution of Swift code and is integrated into the swift compiler. In Xcode, do the following: - Select **File โ–ธ Add Packages...** -- Search for `https://github.com/superwall-me/SuperwallKit-iOS` in the search bar. -- Set the **Dependency Rule** to **Up to Next Major Version** with the lower bound set to **3.0.0** (set this to 2.0.0 if you don't want to use the v3 beta). +- Search for `https://github.com/superwall-me/Superwall-iOS` in the search bar. +- Set the **Dependency Rule** to **branch** with the value set to **master**. If you're wanting to install v2.x, set the **Dependency Rule** to **Up to Next Major Version** with the lower bound set to **2.0.0**. - Make sure your project name is selected in **Add to Project**. - Then, **Add Package**. @@ -76,7 +76,7 @@ The preferred installation method is with [Swift Package Manager](https://swift. To include the *Superwall* SDK in your app, add the following to your Podfile: ``` -pod 'SuperwallKit', '< 4.0.0' +pod 'SuperwallKit', '3.0.0-beta.4' ``` If you don't want to use the v3 beta, you'll need to add this instead: diff --git a/Sources/SuperwallKit/Config/ConfigManager.swift b/Sources/SuperwallKit/Config/ConfigManager.swift index 43ef45cc0..a1f8eb688 100644 --- a/Sources/SuperwallKit/Config/ConfigManager.swift +++ b/Sources/SuperwallKit/Config/ConfigManager.swift @@ -92,6 +92,7 @@ class ConfigManager { /// Gets the assignments from the server and saves them to disk, overwriting any that already exist on disk/in memory. func getAssignments() async { + await $config.hasValue() guard let triggers = config?.triggers, !triggers.isEmpty diff --git a/Sources/SuperwallKit/Config/Options/PaywallOptions.swift b/Sources/SuperwallKit/Config/Options/PaywallOptions.swift index 0d1f3997a..3ceb9e82a 100644 --- a/Sources/SuperwallKit/Config/Options/PaywallOptions.swift +++ b/Sources/SuperwallKit/Config/Options/PaywallOptions.swift @@ -13,7 +13,8 @@ import Foundation public final class PaywallOptions: NSObject { /// Determines whether the paywall should use haptic feedback. Defaults to true. /// - /// Haptic feedback occurs when a user purchases or restores a product, opens a URL from the paywall, or closes the paywall. + /// Haptic feedback occurs when a user purchases or restores a product, opens a URL + /// from the paywall, or closes the paywall. public var isHapticFeedbackEnabled = true /// Enables the sending of non-Superwall tracked events and properties back to the Superwall servers. @@ -27,19 +28,22 @@ public final class PaywallOptions: NSObject { @objc(SWKRestoreFailed) public final class RestoreFailed: NSObject { - /// The title of the alert presented to the user when restoring a transaction fails. Defaults to `No Subscription Found`. + /// The title of the alert presented to the user when restoring a transaction fails. Defaults to + /// `No Subscription Found`. public var title = "No Subscription Found" - /// Defines the message of the alert presented to the user when restoring a transaction fails. Defaults to `We couldn't find an active subscription for your account.` + /// Defines the message of the alert presented to the user when restoring a transaction fails. + /// Defaults to `We couldn't find an active subscription for your account.` public var message = "We couldn't find an active subscription for your account." - /// Defines the title of the close button in the alert presented to the user when restoring a transaction fails. Defaults to `Okay`. + /// Defines the title of the close button in the alert presented to the user when restoring a + /// transaction fails. Defaults to `Okay`. public var closeButtonTitle = "Okay" } /// Defines the messaging of the alert presented to the user when restoring a transaction fails. public var restoreFailed = RestoreFailed() - /// Pre-loads and caches trigger paywalls and products when you initialize the SDK via ``SuperwallKit/Superwall/configure(apiKey:delegate:options:)-65jyx``. Defaults to `true`. + /// Pre-loads and caches trigger paywalls and products when you initialize the SDK via ``Superwall/configure(apiKey:delegate:options:completion:)-7fafw``. Defaults to `true`. /// /// Set this to `false` to load and cache paywalls and products in a just-in-time fashion. /// @@ -49,7 +53,11 @@ public final class PaywallOptions: NSObject { /// Loads paywall template websites from disk, if available. Defaults to `true`. /// - /// When you save a change to your paywall in the Superwall dashboard, a key is appended to the end of your paywall website URL, e.g. `sw_cache_key=`. This is used to cache your paywall webpage to disk after it's first loaded. Superwall will continue to load the cached version of your paywall webpage unless the next time you make a change on the Superwall dashboard. + /// When you save a change to your paywall in the Superwall dashboard, a key is + /// appended to the end of your paywall website URL, e.g. `sw_cache_key=`. + /// This is used to cache your paywall webpage to disk after it's first loaded. Superwall will + /// continue to load the cached version of your paywall webpage unless the next time you + /// make a change on the Superwall dashboard. var useCachedTemplates = false /// Automatically dismisses the paywall when a product is purchased or restored. Defaults to `true`. diff --git a/Sources/SuperwallKit/Config/Options/SuperwallOptions.swift b/Sources/SuperwallKit/Config/Options/SuperwallOptions.swift index 672f1dfce..6616b99bc 100644 --- a/Sources/SuperwallKit/Config/Options/SuperwallOptions.swift +++ b/Sources/SuperwallKit/Config/Options/SuperwallOptions.swift @@ -9,7 +9,7 @@ import Foundation /// Options for configuring Superwall, including paywall presentation and appearance. /// -/// Pass an instance of this class to ``Superwall/configure(apiKey:delegate:options:)-65jyx``. +/// Pass an instance of this class to ``Superwall/configure(apiKey:delegate:options:completion:)-7fafw``. @objc(SWKSuperwallOptions) @objcMembers public final class SuperwallOptions: NSObject { diff --git a/Sources/SuperwallKit/Debug/DebugManager.swift b/Sources/SuperwallKit/Debug/DebugManager.swift index cf5c8dca5..dda00cc87 100644 --- a/Sources/SuperwallKit/Debug/DebugManager.swift +++ b/Sources/SuperwallKit/Debug/DebugManager.swift @@ -24,22 +24,21 @@ final class DebugManager { self.factory = factory } - @MainActor - func handle(deepLinkUrl: URL) { + func handle(deepLinkUrl: URL) -> Bool { guard let launchDebugger = SWDebugManagerLogic.getQueryItemValue( fromUrl: deepLinkUrl, withName: .superwallDebug ) else { - return + return false } guard Bool(launchDebugger) == true else { - return + return false } guard let debugKey = SWDebugManagerLogic.getQueryItemValue( fromUrl: deepLinkUrl, withName: .token ) else { - return + return false } storage.apiKey = debugKey @@ -51,6 +50,7 @@ final class DebugManager { Task { await self.launchDebugger(withPaywallId: paywallId) } + return true } /// Launches the debugger for you to preview paywalls. diff --git a/Sources/SuperwallKit/Delegate/Subscription Controller/SubscriptionController.swift b/Sources/SuperwallKit/Delegate/Subscription Controller/SubscriptionController.swift index d722a1358..2c62823f5 100644 --- a/Sources/SuperwallKit/Delegate/Subscription Controller/SubscriptionController.swift +++ b/Sources/SuperwallKit/Delegate/Subscription Controller/SubscriptionController.swift @@ -14,7 +14,7 @@ import StoreKit /// /// However, if you'd like more control, you can return a ``SubscriptionController`` in /// the delegate when configuring the SDK via -/// ``Superwall/configure(apiKey:delegate:options:)-65jyx``. +/// ``Superwall/configure(apiKey:delegate:options:completion:)-7fafw``. /// /// To learn how to implement the ``SubscriptionController`` in your app /// and best practices, see . diff --git a/Sources/SuperwallKit/Delegate/Subscription Controller/SubscriptionControllerObjc.swift b/Sources/SuperwallKit/Delegate/Subscription Controller/SubscriptionControllerObjc.swift index ebc11da8a..fa1255034 100644 --- a/Sources/SuperwallKit/Delegate/Subscription Controller/SubscriptionControllerObjc.swift +++ b/Sources/SuperwallKit/Delegate/Subscription Controller/SubscriptionControllerObjc.swift @@ -14,7 +14,7 @@ import StoreKit /// /// However, if you'd like more control, you can return a ``SubscriptionControllerObjc`` in /// the delegate when configuring the SDK via -/// ``Superwall/configure(apiKey:delegate:options:)-48l7e``. +/// ``Superwall/configure(apiKey:delegate:options:completion:)-7fafw``. /// /// To learn how to implement the ``SubscriptionControllerObjc`` in your app /// and best practices, see . diff --git a/Sources/SuperwallKit/Delegate/SuperwallDelegate.swift b/Sources/SuperwallKit/Delegate/SuperwallDelegate.swift index 6357240fa..459d830b3 100644 --- a/Sources/SuperwallKit/Delegate/SuperwallDelegate.swift +++ b/Sources/SuperwallKit/Delegate/SuperwallDelegate.swift @@ -11,7 +11,7 @@ import Foundation /// /// The delegate methods receive callbacks from the SDK in response to certain events that happen on the paywall. /// -/// You pass this in when configuring the SDK via ``Superwall/configure(apiKey:delegate:options:)-65jyx``. +/// You pass this in when configuring the SDK via ``Superwall/configure(apiKey:delegate:options:completion:)-7fafw``. /// /// To learn how to conform to the delegate in your app and best practices, see . public protocol SuperwallDelegate: AnyObject { @@ -65,6 +65,21 @@ public protocol SuperwallDelegate: AnyObject { @MainActor func didTrackSuperwallEventInfo(_ info: SuperwallEventInfo) + /// Called when the property ``Superwall/hasActiveSubscription`` changes. + /// + /// This is called whenever the subscription status of the user changes based on the on-device receipt. + /// You can use this to update the state of your application. + /// + /// Alternatively, you can use the published properties of ``Superwall/hasActiveSubscription`` + /// to react to changes as they happen. + /// + /// - Note: If you are implementing a ``SubscriptionController`` to handle your apps subscription-related + /// logic, you should rely on your own subscription status logic and not this method. + /// + /// - Parameters: + /// - newValue: The new value of ``Superwall/hasActiveSubscription``. + func hasActiveSubscriptionDidChange(to newValue: Bool) + /// Receive all the log messages generated by the SDK. /// /// - Parameters: @@ -105,6 +120,8 @@ public extension SuperwallDelegate { func didTrackSuperwallEventInfo(_ info: SuperwallEventInfo) {} + func hasActiveSubscriptionDidChange(to newValue: Bool) {} + func handleLog( level: String, scope: String, diff --git a/Sources/SuperwallKit/Delegate/SuperwallDelegateAdapter.swift b/Sources/SuperwallKit/Delegate/SuperwallDelegateAdapter.swift index 14376e042..c5822193c 100644 --- a/Sources/SuperwallKit/Delegate/SuperwallDelegateAdapter.swift +++ b/Sources/SuperwallKit/Delegate/SuperwallDelegateAdapter.swift @@ -17,10 +17,10 @@ final class SuperwallDelegateAdapter { weak var swiftDelegate: SuperwallDelegate? weak var objcDelegate: SuperwallDelegateObjc? -/// Called on init of the Superwall instance via ``SuperwallKit/Superwall/configure(apiKey:delegate:options:)-7doe5``. +/// Called on init of the Superwall instance via ``Superwall/configure(apiKey:delegate:options:completion:)-7fafw``. /// /// We check to see if the delegates being set are non-nil because they may have been set - /// separately to the initial ``Superwall/configure(apiKey:delegate:options:)-65jyx`` function. + /// separately to the initial ``Superwall/configure(apiKey:delegate:options:completion:)-7fafw`` function. init( swiftDelegate: SuperwallDelegate?, objcDelegate: SuperwallDelegateObjc? @@ -101,6 +101,14 @@ final class SuperwallDelegateAdapter { } } + func hasActiveSubscriptionDidChange(to newValue: Bool) { + if let swiftDelegate = swiftDelegate { + swiftDelegate.hasActiveSubscriptionDidChange(to: newValue) + } else if let objcDelegate = objcDelegate { + objcDelegate.hasActiveSubscriptionDidChange?(to: newValue) + } + } + @MainActor func handleLog( level: String, diff --git a/Sources/SuperwallKit/Delegate/SuperwallDelegateObjc.swift b/Sources/SuperwallKit/Delegate/SuperwallDelegateObjc.swift index 2ec9ae4b0..a1aa1b0dc 100644 --- a/Sources/SuperwallKit/Delegate/SuperwallDelegateObjc.swift +++ b/Sources/SuperwallKit/Delegate/SuperwallDelegateObjc.swift @@ -11,7 +11,7 @@ import Foundation /// /// The delegate methods receive callbacks from the SDK in response to certain events that happen on the paywall. /// -/// You pass this in when configuring the SDK via ``Superwall/configure(apiKey:delegate:options:)-48l7e``. +/// You pass this in when configuring the SDK via ``Superwall/configure(apiKey:delegate:options:completion:)-7fafw``. /// /// To learn how to conform to the delegate in your app and best practices, see . @objc(SWKSuperwallDelegate) @@ -65,6 +65,21 @@ public protocol SuperwallDelegateObjc: AnyObject { @MainActor @objc optional func didTrackSuperwallEventInfo(_ info: SuperwallEventInfo) + /// Called when the property ``Superwall/hasActiveSubscription`` changes. + /// + /// This is called whenever the subscription status of the user changes based on the on-device receipt. + /// You can use this to update the state of your application. + /// + /// Alternatively, you can use the published properties of ``Superwall/hasActiveSubscription`` + /// to react to changes as they happen. + /// + /// - Note: If you are implementing a ``SubscriptionController`` to handle your apps subscription-related + /// logic, you should rely on your own logic and not this method. + /// + /// - Parameters: + /// - newValue: The new value of ``Superwall/hasActiveSubscription``. + @objc optional func hasActiveSubscriptionDidChange(to newValue: Bool) + /// Receive all the log messages generated by the SDK. /// /// - Parameters: diff --git a/Sources/SuperwallKit/Documentation.docc/AdvancedConfiguration.md b/Sources/SuperwallKit/Documentation.docc/AdvancedConfiguration.md index a209da6dc..78e812d4e 100644 --- a/Sources/SuperwallKit/Documentation.docc/AdvancedConfiguration.md +++ b/Sources/SuperwallKit/Documentation.docc/AdvancedConfiguration.md @@ -6,7 +6,7 @@ Use options and custom subscription-related logic for more control over the SDK. By default, Superwall handles all subscription-related logic. However, if you're using RevenueCat, or you just want more control, you can return a ``SuperwallKit/SubscriptionController`` in the delegate when configuring the SDK via - ``Superwall/configure(apiKey:delegate:options:)-48l7e``. In addition, you can customise aspects of the SDK by passing in a ``SuperwallOptions`` object on configure. + ``Superwall/configure(apiKey:delegate:options:completion:)-7fafw``. In addition, you can customise aspects of the SDK by passing in a ``SuperwallOptions`` object on configure. ## Creating a Subscription Controller diff --git a/Sources/SuperwallKit/Documentation.docc/Extensions/SuperwallExtension.md b/Sources/SuperwallKit/Documentation.docc/Extensions/SuperwallExtension.md index 8beb9023b..cbdf4c08d 100644 --- a/Sources/SuperwallKit/Documentation.docc/Extensions/SuperwallExtension.md +++ b/Sources/SuperwallKit/Documentation.docc/Extensions/SuperwallExtension.md @@ -2,7 +2,7 @@ ## Overview -The ``Superwall`` class is used to access all the features of the SDK. Before using any of the features, you must call ``Superwall/configure(apiKey:delegate:options:)-65jyx`` to configure the SDK. +The ``Superwall`` class is used to access all the features of the SDK. Before using any of the features, you must call ``Superwall/configure(apiKey:delegate:options:completion:)-7fafw`` to configure the SDK. ## Topics @@ -11,8 +11,8 @@ The ``Superwall`` class is used to access all the features of the SDK. Before us - - - -- ``configure(apiKey:delegate:options:)-65jyx`` -- ``configure(apiKey:delegate:options:)-48l7e`` +- ``configure(apiKey:delegate:options:completion:)-7fafw`` +- ``configure(apiKey:delegate:options:completion:)-ogg1`` - ``shared`` - ``SuperwallDelegate`` - ``SuperwallDelegateObjc`` @@ -36,7 +36,7 @@ The ``Superwall`` class is used to access all the features of the SDK. Before us - ``publisher(forEvent:params:paywallOverrides:)`` - ``track(event:params:products:ignoreSubscriptionStatus:presentationStyleOverride:onSkip:onPresent:onDismiss:)`` - ``dismiss()`` -- ``dismiss(_:)`` +- ``dismiss(completion:)`` - ``PaywallInfo`` - ``SuperwallEvent`` - ``SuperwallEventObjc`` @@ -51,11 +51,7 @@ The ``Superwall`` class is used to access all the features of the SDK. Before us ### Identifying a User - -- ``createAccount(userId:)`` -- ``logIn(userId:)`` -- ``logIn(userId:completion:)`` -- ``logOut()`` -- ``logOut(completion:)`` +- ``identify(userId:options:)`` - ``reset()`` - ``reset(completion:)`` - ``setUserAttributes(_:)`` @@ -74,11 +70,12 @@ The ``Superwall`` class is used to access all the features of the SDK. Before us - ``SuperwallDelegate/handleLog(level:scope:message:info:error:)-9kmai`` - ``LogLevel`` - ``LogScope`` -- ``SuperwallOptions/Logging-swift.struct`` +- ``SuperwallOptions/Logging-swift.class`` ### Customization - ``localizationOverride(localeIdentifier:)`` +- ``togglePaywallSpinner(isHidden:)`` ### Helper Variables - ``presentedViewController`` diff --git a/Sources/SuperwallKit/Documentation.docc/GettingStarted.md b/Sources/SuperwallKit/Documentation.docc/GettingStarted.md index f85bbec81..2392d8d6b 100644 --- a/Sources/SuperwallKit/Documentation.docc/GettingStarted.md +++ b/Sources/SuperwallKit/Documentation.docc/GettingStarted.md @@ -4,7 +4,7 @@ Configuring the SDK. ## Overview -To get up and running, you need to get your **API Key** from the Superwall Dashboard. You then configure the SDK using ``Superwall/configure(apiKey:delegate:options:)-65jyx`` and then present your paywall. +To get up and running, you need to get your **API Key** from the Superwall Dashboard. You then configure the SDK using ``Superwall/configure(apiKey:delegate:options:completion:)-7fafw`` and then present your paywall. ## Getting your API Key @@ -17,18 +17,18 @@ On that page, you will see your **Public API Key**. Copy this for the next step. ### Configuring the SDK -To configure the SDK, you must call ``Superwall/configure(apiKey:delegate:options:)-65jyx`` as soon as your app launches from `application(_:didFinishLaunchingWithOptions:)`. We recommended creating a service class **SuperwallService.swift** that handles your SDK configuration: +To configure the SDK, you must call ``Superwall/configure(apiKey:delegate:options:completion:)-7fafw`` as soon as your app launches from `application(_:didFinishLaunchingWithOptions:)`: ```swift import SuperwallKit -final class SuperwallService { - private static let apiKey = "MYAPIKEY" // Replace this with your API Key - static let shared = SuperwallService() - - static func initialize() { - Superwall.configure(apiKey: apiKey) - } +final class AppDelegate: UIResponder, UIApplicationDelegate { + func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]? + ) -> Bool { +Superwall.configure(apiKey: "MYAPIKEY") // Replace this with your API Key + ) } ``` @@ -40,26 +40,11 @@ Superwall handles all the subscription-related logic for you. However, if you'd We generate a random user ID that persists internally until the user deletes/reinstalls your app. -If you use your own user management system, call ``Superwall/createAccount(userId:)`` when a user first creates an account, and ``Superwall/logIn(userId:)`` if you're logging in an existing user. This will alias your `userId` with the anonymous Superwall ID enabling us to load the user's assigned paywalls. - -Calling ``Superwall/logOut()`` or ``Superwall/reset()`` will reset the on-device `userId` to a random ID and clear the on-device paywall assignments. - -## Configuring From the App Delegate - -Next, call `SuperwallService.initialize()` from `application(_:didFinishLaunchingWithOptions:)` in your App Delegate: +If you use your own user management system, call ``Superwall/identify(userId:options:)`` when a user creates or logs in to an account. This will alias your `userId` with the anonymous Superwall ID enabling us to load the user's assigned paywalls. -```swift -import SuperwallKit +Calling ``Superwall/reset()`` will reset the on-device `userId` to a random ID and clear the on-device paywall assignments. Yuu should do this when logging out or wanting to reset the identity of anonymous users. -final class AppDelegate: UIResponder, UIApplicationDelegate { - func application( - _ application: UIApplication, - didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]? - ) -> Bool { - SuperwallService.initialize() - ) -} -``` +- Note: You can pass an ``IdentityOptions`` object to ``Superwall/identify(userId:options:)``. This should only be used in advanced use cases. By setting the ``IdentityOptions/restorePaywallAssignments`` property of ``IdentityOptions`` to `true`, paywalls are prevented from showing until after paywall assignments have been restored. If you expect users of your app to switch accounts or delete/reinstall a lot, you'd set this when users log in to an existing account. You're now ready to track an event to present your first paywall. See for next steps. diff --git a/Sources/SuperwallKit/Documentation.docc/InAppPreviews.md b/Sources/SuperwallKit/Documentation.docc/InAppPreviews.md index dcbf68fcf..45782c62a 100644 --- a/Sources/SuperwallKit/Documentation.docc/InAppPreviews.md +++ b/Sources/SuperwallKit/Documentation.docc/InAppPreviews.md @@ -14,17 +14,7 @@ In your `info.plist`, you'll need to add a custom URL scheme for your app: You can view [Apple's documentation](https://developer.apple.com/documentation/xcode/defining-a-custom-url-scheme-for-your-app) to learn more about how to do that. -Then, you'll need to handle the deep link within your app using ``Superwall/handleDeepLink(_:)``. We recommend adding this to your `SuperwallService.swift` file that handles all Superwall related functions: - -```swift -extension SuperwallService { - static func handleDeepLink(_ url: URL) { - Superwall.shared.handleDeepLink(url) - } -} -``` - -Then, you'll need to call this when your app is opened via a deep link. There are different ways to do this, depending on whether you're using a SceneDelegate, AppDelegate, or writing an app in SwiftUI. +Then, you'll need to handle the deep link within your app using ``Superwall/handleDeepLink(_:)``. You'll need to call this when your app is opened via a deep link. There are different ways to do this, depending on whether you're using a SceneDelegate, AppDelegate, or writing an app in SwiftUI. ### Handling a Deep Link in SwiftUI @@ -39,7 +29,7 @@ struct MyApp: App { WindowGroup { ContentView() .onOpenURL { url in - SuperwallService.handleDeepLink(url) + Superwall.shared.handleDeepLink(url) } } } @@ -57,8 +47,7 @@ func application( _ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:] ) -> Bool { - SuperwallService.handleDeepLink(url) - return true + return Superwall.shared.handleDeepLink(url) } ``` @@ -78,7 +67,7 @@ func scene( ... for context in connectionOptions.urlContexts { - SuperwallService.handleDeepLink(context.url) + Superwall.shared.handleDeepLink(context.url) } } @@ -88,7 +77,7 @@ func scene( openURLContexts URLContexts: Set ) { for context in URLContexts { - SuperwallService.handleDeepLink(context.url) + Superwall.shared.handleDeepLink(context.url) } } ``` diff --git a/Sources/SuperwallKit/Documentation.docc/SuperwallEvents.md b/Sources/SuperwallKit/Documentation.docc/SuperwallEvents.md index 8078152de..c7a03a7b2 100644 --- a/Sources/SuperwallKit/Documentation.docc/SuperwallEvents.md +++ b/Sources/SuperwallKit/Documentation.docc/SuperwallEvents.md @@ -8,7 +8,7 @@ The SDK automatically tracks the events specified in ``SuperwallEvent``. Some of Event Name | Action | Can Present Paywalls --- | --- | --- -`app_install` | When the SDK is configured for the first time, or directly after calling ``Superwall/logOut()`` or ``Superwall/reset()``. | *Yes* +`app_install` | When the SDK is configured for the first time, or directly after calling ``Superwall/reset()``. | *Yes* `app_launch` | When the app is launched from a cold start. | *Yes* `session_start` | When the app is opened either from a cold start, or after at least 30 seconds since last `app_close`. | *Yes* (recommended) `deepLink_open` | When a user opens the app via a deep link. | *Yes* diff --git a/Sources/SuperwallKit/Graveyard/SuperwallGraveyard.swift b/Sources/SuperwallKit/Graveyard/SuperwallGraveyard.swift index 7d1f5dc9e..cc2eb80a8 100644 --- a/Sources/SuperwallKit/Graveyard/SuperwallGraveyard.swift +++ b/Sources/SuperwallKit/Graveyard/SuperwallGraveyard.swift @@ -10,7 +10,7 @@ import UIKit public extension Superwall { // MARK: - Unavailable methods // TODO: Fix deprecation here - /*@available(*, unavailable, renamed: "configure(apiKey:delegate:options:)") + /*@available(*, unavailable, renamed: "configure(apiKey:delegate:options:completion:)") @discardableResult @objc static func configure( apiKey: String, @@ -21,12 +21,6 @@ public extension Superwall { return shared }*/ - @available(*, unavailable, message: "This has been removed. Please use login(userId:) or createAccount(userId:).") - @discardableResult - @objc static func identify(userId: String) -> Superwall { - return shared - } - @available(*, unavailable, renamed: "preloadPaywalls(forEvents:)") @objc static func preloadPaywalls(forTriggers triggers: Set) {} diff --git a/Sources/SuperwallKit/Identity/IdentityError.swift b/Sources/SuperwallKit/Identity/IdentityError.swift index ff523399f..9329d68b3 100644 --- a/Sources/SuperwallKit/Identity/IdentityError.swift +++ b/Sources/SuperwallKit/Identity/IdentityError.swift @@ -10,16 +10,6 @@ import Foundation /// The error returned when trying to create an account. @objc(SWKIdentityError) public enum IdentityError: Int, Error { - /// The user is already logged in. - case alreadyLoggedIn - /// The `userId` that was provided was empty. case missingUserId } - -/// The error returned when trying to logout a user out. -@objc(SWKLogoutError) -public enum LogoutError: Int, Error { - /// The user isn't logged in. - case notLoggedIn -} diff --git a/Sources/SuperwallKit/Identity/IdentityManager.swift b/Sources/SuperwallKit/Identity/IdentityManager.swift index 9a99fae08..85885a5f5 100644 --- a/Sources/SuperwallKit/Identity/IdentityManager.swift +++ b/Sources/SuperwallKit/Identity/IdentityManager.swift @@ -77,54 +77,58 @@ class IdentityManager { didSetIdentity() } - /// Logs user in and waits for config then assignments. + /// Create an account and may or may not wait for assignments before + /// returning. /// /// - Throws: An error of type ``IdentityError``. - func logIn(userId: String) async throws { - guard appUserId == nil else { - throw IdentityError.alreadyLoggedIn + func identify( + userId: String, + options: IdentityOptions? + ) async throws { + guard let sanitizedUserId = sanitize(userId: userId) else { + throw IdentityError.missingUserId } - identitySubject.send(false) - - guard let appUserId = sanitize(userId: userId) else { - throw IdentityError.missingUserId + // If they're sending the same userId as before, then they're + // already logged in. + if appUserId == userId { + return } - self.appUserId = appUserId - await configManager.$config.hasValue() - await configManager.getAssignments() + identitySubject.send(false) - didSetIdentity() - } + let oldUserId = appUserId - /// Create an account but don't wait for assignments before returning. - /// - /// - Throws: An error of type ``IdentityError``. - func createAccount(userId: String) throws { - guard appUserId == nil else { - throw IdentityError.alreadyLoggedIn + // If user already logged in but identifying with a different + // different userId, reset everything first. + if oldUserId != nil, + userId != oldUserId { + await Superwall.shared.reset() } - identitySubject.send(false) - guard let appUserId = sanitize(userId: userId) else { - throw IdentityError.missingUserId - } - self.appUserId = appUserId + appUserId = sanitizedUserId - didSetIdentity() - } + // If they have set restore paywall assignments to true, + // Wait for assignments before setting identity. Otherwise, + // get assignments in the background. - /// Logs user out and calls ``SuperwallKit/Superwall/reset()`` - /// - /// - Throws: An error of type``LogoutError``. - /// if the user isn't logged in. - func logOut() async throws { - if appUserId == nil { - throw LogoutError.notLoggedIn + func getAssignmentsAsync() { + Task.detached { + await self.configManager.getAssignments() + } + didSetIdentity() } - await Superwall.shared.reset() + if let options = options { + if options.restorePaywallAssignments { + await configManager.getAssignments() + didSetIdentity() + } else { + getAssignmentsAsync() + } + } else { + getAssignmentsAsync() + } } /// Clears all stored user-specific variables. diff --git a/Sources/SuperwallKit/Identity/IdentityOptions.swift b/Sources/SuperwallKit/Identity/IdentityOptions.swift new file mode 100644 index 000000000..d4798a4a2 --- /dev/null +++ b/Sources/SuperwallKit/Identity/IdentityOptions.swift @@ -0,0 +1,22 @@ +// +// File.swift +// +// +// Created by Yusuf Tรถr on 02/02/2023. +// + +import Foundation + +/// Options passed in when calling ``Superwall/identify(userId:options:)``. +@objc(SWKIdentityOptions) +@objcMembers +public final class IdentityOptions: NSObject { + /// Determines whether the SDK should wait to restore paywall assignments from the server + /// before presenting any paywalls. + /// + /// This should only be used in advanced use cases. By setting this to `true`, it prevents + /// paywalls from showing until after paywall assignments have been restored. If you expect + /// users of your app to switch accounts or delete/reinstall a lot, you'd set this when users log + /// in to an existing account. + public var restorePaywallAssignments = false +} diff --git a/Sources/SuperwallKit/Identity/PublicIdentity.swift b/Sources/SuperwallKit/Identity/PublicIdentity.swift index 6ef36c854..8f99f863f 100644 --- a/Sources/SuperwallKit/Identity/PublicIdentity.swift +++ b/Sources/SuperwallKit/Identity/PublicIdentity.swift @@ -7,95 +7,41 @@ import Foundation -// MARK: - Log In -public extension Superwall { - /// Logs in a user with their `userId` to retrieve paywalls that they've been assigned to. - /// - /// This links a `userId` to Superwall's automatically generated alias. Call this once after you've retrieved a userId. - /// - /// The user will stay logged in until you call ``SuperwallKit/Superwall/logOut()``. If you call this while they're already logged in, it will throw an error of type ``IdentityError``. - /// - Parameter userId: Your user's unique identifier, as defined by your backend system. - /// - Throws: An error of type ``IdentityError``. - @objc func logIn(userId: String) async throws { - try await dependencyContainer.identityManager.logIn(userId: userId) - } - - /// Logs in a user with their `userId` to retrieve paywalls that they've been assigned to. - /// - /// This links a `userId` to Superwall's automatically generated alias. Call this as soon - /// as you have a userId. If a user with a different id was previously identified, calling this - /// will automatically call ``reset()`` - /// - Parameters: - /// - userId: Your user's unique identifier, as defined by your backend system. - /// - completion: A completion block that accepts a `Result` enum. Its success value is - /// the shared Superwall instance, and its failure error is of type ``IdentityError``. - func logIn( - userId: String, - completion: ((Result) -> Void)? - ) { - Task { - do { - try await logIn(userId: userId) - await MainActor.run { - completion?(.success(())) - } - } catch let error as IdentityError { - await MainActor.run { - completion?(.failure(error)) - } - } - } - } -} - - -// MARK: - Create Account +// MARK: - Identify public extension Superwall { /// Creates an account with Superwall. This links a `userId` to Superwall's automatically generated alias. /// - /// Call this as soon as you have a `userId`. If you are logging in an existing user, you should use - /// ``Superwall/logIn(userId:)`` instead, as that will retrieve their assigned paywalls. + /// Call this as soon as you have a `userId`. /// - /// - Parameter userId: Your user's unique identifier, as defined by your backend system. + /// - Parameters: + /// - userId: Your user's unique identifier, as defined by your backend system. + /// - options: An ``IdentityOptions`` object, whose property + /// ``IdentityOptions/restorePaywallAssignments`` you can set to `true` + /// to tell the SDK to wait to restore paywall assignments from the server before presenting any paywalls. + /// This should only be used in advanced use cases. If you expect + /// users of your app to switch accounts or delete/reinstall a lot, you'd set this when users log in to an + /// existing account. /// - Throws: An error of type ``IdentityError``. - @objc func createAccount(userId: String) throws { - try dependencyContainer.identityManager.createAccount(userId: userId) - } -} - -// MARK: - Log Out -public extension Superwall { - /// Logs out the user. This calls ``reset()``, which resets on-device paywall - /// assignments and the `userId` stored by Superwall. - /// - /// You must call this method before attempting to log in a new user. - /// If a user isn't already logged in before calling this method, an error will be thrown. - /// - /// - Throws: An error of type ``LogoutError``. - @objc func logOut() async throws { - try await dependencyContainer.identityManager.logOut() + @objc func identify( + userId: String, + options: IdentityOptions? = nil + ) async throws { + try await dependencyContainer.identityManager.identify( + userId: userId, + options: options + ) } - /// Logs out the user. This calls ``reset()``, which resets on-device paywall - /// assignments and the `userId` stored by Superwall. - /// - /// You must call this method before attempting to log in a new user. - /// If a user isn't already logged in before calling this method, an error will be thrown. - /// - /// - Parameters: - /// - completion: A completion block that accepts a `Result` object. - /// The `Result`'s success value is `Void` and failure error is of type ``LogoutError``. - func logOut(completion: ((Result) -> Void)? = nil) { + @nonobjc + func identify( + userId: String, + options: IdentityOptions? = nil, + completion: (() -> Void)? = nil + ) throws { Task { - do { - try await logOut() - await MainActor.run { - completion?(.success(())) - } - } catch let error as LogoutError { - await MainActor.run { - completion?(.failure(error)) - } + try await identify(userId: userId, options: options) + await MainActor.run { + completion?() } } } @@ -108,16 +54,18 @@ public extension Superwall { @objc func reset() async { presentationItems.reset() dependencyContainer.identityManager.reset() - dependencyContainer.storage.reset() + await dependencyContainer.storage.reset() await dependencyContainer.paywallManager.resetCache() dependencyContainer.configManager.reset() dependencyContainer.identityManager.didSetIdentity() } - /// Asynchronously resets the `userId` and data stored by Superwall. + /// Resets the `userId`, on-device paywall assignments, and data stored + /// by Superwall. /// /// - Parameters: /// - completion: A completion block that is called when reset has completed. + @nonobjc func reset(completion: (() -> Void)? = nil) { Task { await reset() diff --git a/Sources/SuperwallKit/Misc/Constants.swift b/Sources/SuperwallKit/Misc/Constants.swift index 7ff351aef..f6c3aade6 100644 --- a/Sources/SuperwallKit/Misc/Constants.swift +++ b/Sources/SuperwallKit/Misc/Constants.swift @@ -18,5 +18,5 @@ let sdkVersion = """ */ let sdkVersion = """ -3.0.0-beta.3 +3.0.0-beta.4 """ diff --git a/Sources/SuperwallKit/Paywall/Manager/PaywallManager.swift b/Sources/SuperwallKit/Paywall/Manager/PaywallManager.swift index 02b9af1a8..5572f27a6 100644 --- a/Sources/SuperwallKit/Paywall/Manager/PaywallManager.swift +++ b/Sources/SuperwallKit/Paywall/Manager/PaywallManager.swift @@ -27,7 +27,7 @@ class PaywallManager { } @MainActor - func removePaywall(identifier: String?) { + func removePaywallViewController(identifier: String?) { cache.removePaywallViewController(identifier: identifier) } diff --git a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/InternalPresentation.swift b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/InternalPresentation.swift index 0a559ad84..06293ef8a 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/InternalPresentation.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/Internal Presentation/InternalPresentation.swift @@ -54,16 +54,14 @@ extension Superwall { } /// Presents the paywall again by sending the previous presentation request to the presentation publisher. - func presentAgain() async { + func presentAgain(identifier: String) async { guard let lastPresentationItems = presentationItems.last else { return } // Remove the currently presenting paywall from cache. await MainActor.run { - if let presentingPaywallIdentifier = Superwall.shared.paywallViewController?.paywall.identifier { - dependencyContainer.paywallManager.removePaywall(identifier: presentingPaywallIdentifier) - } + dependencyContainer.paywallManager.removePaywallViewController(identifier: identifier) } // Resend the request and pass in the state publisher so it can continue diff --git a/Sources/SuperwallKit/Paywall/Presentation/PublicPresentation.swift b/Sources/SuperwallKit/Paywall/Presentation/PublicPresentation.swift index 442625e0c..169892154 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/PublicPresentation.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/PublicPresentation.swift @@ -19,7 +19,7 @@ public extension Superwall { /// - Parameters: /// - completion: An optional completion block that gets called after the paywall is dismissed. Defaults to nil. @MainActor - func dismiss(_ completion: (() -> Void)? = nil) { + func dismiss(completion: (() -> Void)? = nil) { guard let paywallViewController = paywallViewController else { return } diff --git a/Sources/SuperwallKit/Paywall/Request/PaywallRequestManager.swift b/Sources/SuperwallKit/Paywall/Request/PaywallRequestManager.swift index 9d052517f..c6001cc6c 100644 --- a/Sources/SuperwallKit/Paywall/Request/PaywallRequestManager.swift +++ b/Sources/SuperwallKit/Paywall/Request/PaywallRequestManager.swift @@ -47,7 +47,7 @@ actor PaywallRequestManager { shouldUseCache { // Calculate whether there's a free trial available if let primaryProduct = paywall.products.first(where: { $0.type == .primary }), - let storeProduct = storeKitManager.productsById[primaryProduct.id] { + let storeProduct = await storeKitManager.productsById[primaryProduct.id] { let isFreeTrialAvailable = storeKitManager.isFreeTrialAvailable(for: storeProduct) paywall.isFreeTrialAvailable = isFreeTrialAvailable } diff --git a/Sources/SuperwallKit/Paywall/View Controller/Loading/HiddenListener.swift b/Sources/SuperwallKit/Paywall/View Controller/Loading/Listeners/HiddenListener.swift similarity index 81% rename from Sources/SuperwallKit/Paywall/View Controller/Loading/HiddenListener.swift rename to Sources/SuperwallKit/Paywall/View Controller/Loading/Listeners/HiddenListener.swift index 1b5313a8a..583ade847 100644 --- a/Sources/SuperwallKit/Paywall/View Controller/Loading/HiddenListener.swift +++ b/Sources/SuperwallKit/Paywall/View Controller/Loading/Listeners/HiddenListener.swift @@ -10,7 +10,6 @@ import SwiftUI struct HiddenListenerViewModifier: ViewModifier { let isHidden: Published.Publisher let model: LoadingModel - let maxPadding: CGFloat func body(content: Content) -> some View { content @@ -24,29 +23,25 @@ struct HiddenListenerViewModifier: ViewModifier { model.isAnimating = true model.scaleAmount = 1 model.rotationAmount = 0 - model.padding = maxPadding } private func hide() { model.scaleAmount = 0.05 model.rotationAmount = .pi model.isAnimating = false - model.padding = 0 } } extension View { func listen( to isHidden: Published.Publisher, - fromModel model: LoadingModel, - maxPadding: CGFloat + fromModel model: LoadingModel ) -> some View { ModifiedContent( content: self, modifier: HiddenListenerViewModifier( isHidden: isHidden, - model: model, - maxPadding: maxPadding + model: model ) ) } diff --git a/Sources/SuperwallKit/Paywall/View Controller/Loading/Listeners/PaddingListener.swift b/Sources/SuperwallKit/Paywall/View Controller/Loading/Listeners/PaddingListener.swift new file mode 100644 index 000000000..911736e85 --- /dev/null +++ b/Sources/SuperwallKit/Paywall/View Controller/Loading/Listeners/PaddingListener.swift @@ -0,0 +1,43 @@ +// +// File.swift +// +// +// Created by Yusuf Tรถr on 01/02/2023. +// + +import SwiftUI + +struct PaddingListenerViewModifier: ViewModifier { + let movedUp: Published.Publisher + let model: LoadingModel + let maxPadding: CGFloat + + func body(content: Content) -> some View { + content + .onAppear() + .onReceive(movedUp) { movedUp in + if movedUp { + model.padding = maxPadding + } else { + model.padding = 0 + } + } + } +} + +extension View { + func listen( + to movedUp: Published.Publisher, + fromModel model: LoadingModel, + maxPadding: CGFloat + ) -> some View { + ModifiedContent( + content: self, + modifier: PaddingListenerViewModifier( + movedUp: movedUp, + model: model, + maxPadding: maxPadding + ) + ) + } +} diff --git a/Sources/SuperwallKit/Paywall/View Controller/Loading/LoadingModel.swift b/Sources/SuperwallKit/Paywall/View Controller/Loading/LoadingModel.swift index 2338d63f3..6256046bb 100644 --- a/Sources/SuperwallKit/Paywall/View Controller/Loading/LoadingModel.swift +++ b/Sources/SuperwallKit/Paywall/View Controller/Loading/LoadingModel.swift @@ -13,9 +13,41 @@ final class LoadingModel: ObservableObject { @Published var rotationAmount: CGFloat = .pi @Published var padding: CGFloat = 0 @Published var isHidden = false + @Published var movedUp = false private weak var delegate: LoadingDelegate? init(delegate: LoadingDelegate?) { self.delegate = delegate + addObservers() + } + + /// Add observers so that we know when the payment sheet will display. + private func addObservers() { + NotificationCenter.default.addObserver( + self, + selector: #selector(applicationWillResignActive), + name: UIApplication.willResignActiveNotification, + object: nil + ) + NotificationCenter.default.addObserver( + self, + selector: #selector(applicationDidBecomeActive), + name: UIApplication.didBecomeActiveNotification, + object: nil + ) + } + + @objc private func applicationWillResignActive() { + guard delegate?.loadingState == .loadingPurchase else { + return + } + movedUp = true + } + + @objc private func applicationDidBecomeActive() { + guard delegate?.loadingState == .loadingPurchase else { + return + } + movedUp = false } } diff --git a/Sources/SuperwallKit/Paywall/View Controller/Loading/LoadingView.swift b/Sources/SuperwallKit/Paywall/View Controller/Loading/LoadingView.swift index 99bad9d4f..f38b9fcf4 100644 --- a/Sources/SuperwallKit/Paywall/View Controller/Loading/LoadingView.swift +++ b/Sources/SuperwallKit/Paywall/View Controller/Loading/LoadingView.swift @@ -20,10 +20,14 @@ struct LoadingView: View { .scaleAnimation(for: model.scaleAmount) .bottomPaddingAnimation(for: model.padding) .listen( - to: model.$isHidden, + to: model.$movedUp, fromModel: model, maxPadding: proxy.size.height / 2 ) + .listen( + to: model.$isHidden, + fromModel: model + ) .frame( maxWidth: .infinity, maxHeight: .infinity diff --git a/Sources/SuperwallKit/Paywall/View Controller/PaywallViewController.swift b/Sources/SuperwallKit/Paywall/View Controller/PaywallViewController.swift index a4a387499..01f04a96a 100644 --- a/Sources/SuperwallKit/Paywall/View Controller/PaywallViewController.swift +++ b/Sources/SuperwallKit/Paywall/View Controller/PaywallViewController.swift @@ -20,9 +20,19 @@ protocol PaywallViewControllerDelegate: AnyObject { } enum PaywallLoadingState { + /// The initial state of the paywall case unknown + + /// When a purchase is loading case loadingPurchase - case loadingResponse + + /// When the paywall URL is loading + case loadingURL + + /// When the user has manually shown the spinner + case manualLoading + + /// When everything has loaded. case ready } @@ -233,9 +243,12 @@ class PaywallViewController: UIViewController, SWWebViewDelegate, LoadingDelegat state: .closed ), shouldSendDismissedState: false - ) { + ) { [weak self] in + guard let self = self else { + return + } Task { - await Superwall.shared.presentAgain() + await Superwall.shared.presentAgain(identifier: self.paywall.identifier) } } } @@ -282,17 +295,30 @@ class PaywallViewController: UIViewController, SWWebViewDelegate, LoadingDelegat webView.load(request) } - loadingState = .loadingResponse + loadingState = .loadingURL } // MARK: - State Handling + func togglePaywallSpinner(isHidden: Bool) { + if isHidden { + if loadingState == .manualLoading { + loadingState = .ready + } + } else { + if loadingState == .ready { + loadingState = .manualLoading + } + } + } + func loadingStateDidChange(from oldValue: PaywallLoadingState) { switch loadingState { case .unknown: break - case .loadingPurchase: + case .loadingPurchase, + .manualLoading: addLoadingView() - case .loadingResponse: + case .loadingURL: addShimmerView() showRefreshButtonAfterTimeout(true) UIView.springAnimate { @@ -301,11 +327,12 @@ class PaywallViewController: UIViewController, SWWebViewDelegate, LoadingDelegat } case .ready: let translation = CGAffineTransform.identity.translatedBy(x: 0, y: 10) - webView.transform = oldValue == .loadingPurchase ? .identity : translation + let spinnerDidShow = oldValue == .loadingPurchase || oldValue == .manualLoading + webView.transform = spinnerDidShow ? .identity : translation showRefreshButtonAfterTimeout(false) hideLoadingView() - if oldValue != .loadingPurchase { + if !spinnerDidShow { UIView.animate( withDuration: 1, delay: 0.25, @@ -327,7 +354,7 @@ class PaywallViewController: UIViewController, SWWebViewDelegate, LoadingDelegat guard shimmerView == nil else { return } - guard loadingState == .loadingResponse || loadingState == .unknown else { + guard loadingState == .loadingURL || loadingState == .unknown else { return } guard isActive || onPresent else { @@ -537,7 +564,7 @@ class PaywallViewController: UIViewController, SWWebViewDelegate, LoadingDelegat present(alertController, animated: true) { [weak self] in if let loadingState = self?.loadingState, - loadingState != .loadingResponse { + loadingState != .loadingURL { self?.loadingState = .ready } } @@ -670,7 +697,8 @@ extension PaywallViewController { shouldSendDismissedState: Bool = true, completion: (() -> Void)? = nil ) { - if self.loadingState == .loadingPurchase { + let isShowingSpinner = loadingState == .loadingPurchase || loadingState == .manualLoading + if isShowingSpinner { self.loadingState = .ready } diff --git a/Sources/SuperwallKit/Storage/Cache/Cache.swift b/Sources/SuperwallKit/Storage/Cache/Cache.swift index 866c97388..71b84f301 100644 --- a/Sources/SuperwallKit/Storage/Cache/Cache.swift +++ b/Sources/SuperwallKit/Storage/Cache/Cache.swift @@ -243,33 +243,36 @@ class Cache { // MARK: - Clean extension Cache { /// Clean all mem cache and disk cache. This is an async operation. - func cleanUserFiles() { + func cleanUserFiles() async { cleanMemCache() - cleanDiskCache() + await cleanDiskCache() } private func cleanMemCache() { memCache.removeAllObjects() } - private func cleanDiskCache() { - ioQueue.async { [weak self] in - guard let self = self else { - return - } - do { - if self.fileManager.fileExists(atPath: self.cacheUrl.path) { - try self.fileManager.removeItem(atPath: self.cacheUrl.path) + private func cleanDiskCache() async { + await withCheckedContinuation { continuation in + ioQueue.async { [weak self] in + guard let self = self else { + return } - if self.fileManager.fileExists(atPath: self.userSpecificDocumentUrl.path) { - try self.fileManager.removeItem(atPath: self.userSpecificDocumentUrl.path) + do { + if self.fileManager.fileExists(atPath: self.cacheUrl.path) { + try self.fileManager.removeItem(atPath: self.cacheUrl.path) + } + if self.fileManager.fileExists(atPath: self.userSpecificDocumentUrl.path) { + try self.fileManager.removeItem(atPath: self.userSpecificDocumentUrl.path) + } + } catch { + Logger.debug( + logLevel: .error, + scope: .cache, + message: "Error when clean disk: \(error.localizedDescription)" + ) } - } catch { - Logger.debug( - logLevel: .error, - scope: .cache, - message: "Error when clean disk: \(error.localizedDescription)" - ) + continuation.resume() } } } diff --git a/Sources/SuperwallKit/Storage/Core Data/CoreDataManager.swift b/Sources/SuperwallKit/Storage/Core Data/CoreDataManager.swift index ac214ba0a..097724eea 100644 --- a/Sources/SuperwallKit/Storage/Core Data/CoreDataManager.swift +++ b/Sources/SuperwallKit/Storage/Core Data/CoreDataManager.swift @@ -60,7 +60,7 @@ class CoreDataManager { } } - func deleteAllEntities(completion: (() -> Void)? = nil) { + func deleteAllEntities() async { let eventDataRequest: NSFetchRequest = NSFetchRequest( entityName: ManagedEventData.entityName ) @@ -72,18 +72,20 @@ class CoreDataManager { let deleteOccurrenceRequest = NSBatchDeleteRequest(fetchRequest: occurrenceRequest) let container = coreDataStack.persistentContainer - container.performBackgroundTask { context in - do { - try context.executeAndMergeChanges(using: deleteEventDataRequest) - try context.executeAndMergeChanges(using: deleteOccurrenceRequest) - completion?() - } catch { - Logger.debug( - logLevel: .error, - scope: .coreData, - message: "Could not delete core data.", - error: error - ) + await withCheckedContinuation { continuation in + container.performBackgroundTask { context in + do { + try context.executeAndMergeChanges(using: deleteEventDataRequest) + try context.executeAndMergeChanges(using: deleteOccurrenceRequest) + } catch { + Logger.debug( + logLevel: .error, + scope: .coreData, + message: "Could not delete core data.", + error: error + ) + } + continuation.resume() } } } diff --git a/Sources/SuperwallKit/Storage/Storage.swift b/Sources/SuperwallKit/Storage/Storage.swift index 438acb65c..f6ff299e4 100644 --- a/Sources/SuperwallKit/Storage/Storage.swift +++ b/Sources/SuperwallKit/Storage/Storage.swift @@ -74,9 +74,9 @@ class Storage { } /// Clears data that is user specific. - func reset() { - coreDataManager.deleteAllEntities() - cache.cleanUserFiles() + func reset() async { + await coreDataManager.deleteAllEntities() + await cache.cleanUserFiles() confirmedAssignments = nil didTrackFirstSeen = false recordFirstSeenTracked() diff --git a/Sources/SuperwallKit/StoreKit/Products/Receipt Manager/Receipt Models/InAppPurchase.swift b/Sources/SuperwallKit/StoreKit/Products/Receipt Manager/Receipt Models/InAppPurchase.swift index a5bd02229..2093d37a7 100644 --- a/Sources/SuperwallKit/StoreKit/Products/Receipt Manager/Receipt Models/InAppPurchase.swift +++ b/Sources/SuperwallKit/StoreKit/Products/Receipt Manager/Receipt Models/InAppPurchase.swift @@ -21,7 +21,13 @@ struct InAppPurchase: ASN1Decodable, Hashable { /// - Parameter date: The date in which the auto-renewable subscription should be active. /// - Returns: true if the latest auto-renewable subscription is active for the given date, false otherwise. var isActive: Bool { - assert(subscriptionExpirationDate != nil, "\(productIdentifier) is not an auto-renewable subscription.") + // If has no expiration date, assume it's a lifetime purchase. + // It might not be - it could be another non-consumable OR a + // consumable. But for those use cases they should handle the logic + // themselves. + if subscriptionExpirationDate == nil { + return true + } if cancellationDate != nil { return false } diff --git a/Sources/SuperwallKit/StoreKit/StoreKitManager.swift b/Sources/SuperwallKit/StoreKit/StoreKitManager.swift index 0b75442df..dac9b3f90 100644 --- a/Sources/SuperwallKit/StoreKit/StoreKitManager.swift +++ b/Sources/SuperwallKit/StoreKit/StoreKitManager.swift @@ -1,12 +1,12 @@ import Foundation import Combine -final class StoreKitManager { +actor StoreKitManager { /// Coordinates: The purchasing, restoring and retrieving of products; the checking /// of transactions; and the determining of the user's subscription status. - lazy var coordinator = factory.makeStoreKitCoordinator() - private unowned let factory: StoreKitCoordinatorFactory - private lazy var receiptManager = ReceiptManager(delegate: self) + nonisolated lazy var coordinator = factory.makeStoreKitCoordinator() + private nonisolated unowned let factory: StoreKitCoordinatorFactory + private nonisolated lazy var receiptManager = ReceiptManager(delegate: self) private(set) var productsById: [String: StoreProduct] = [:] private struct ProductProcessingResult { @@ -37,39 +37,6 @@ final class StoreKitManager { return variables } - /// This refreshes the device receipt. - /// - /// - Warning: This will prompt the user to log in, so only do this on - /// when restoring or after purchasing. - @discardableResult - func refreshReceipt() async -> Bool { - Logger.debug( - logLevel: .debug, - scope: .storeKitManager, - message: "Refreshing App Store receipt." - ) - return await receiptManager.refreshReceipt() - } - - /// Loads the purchased products from the receipt, - func loadPurchasedProducts() async { - Logger.debug( - logLevel: .debug, - scope: .storeKitManager, - message: "Loading purchased products from the App Store receipt." - ) - await receiptManager.loadPurchasedProducts() - } - - /// Determines whether a free trial is available based on the product the user is purchasing. - /// - /// A free trial is available if the user hasn't already purchased within the subscription group of the - /// supplied product. If it isn't a subscription-based product or there are other issues retrieving the products, - /// the outcome will default to whether or not the user has already purchased that product. - func isFreeTrialAvailable(for product: StoreProduct) -> Bool { - return receiptManager.isFreeTrialAvailable(for: product) - } - func getProducts( withIds responseProductIds: [String], responseProducts: [Product] = [], @@ -147,9 +114,46 @@ final class StoreKitManager { } } +// MARK: - Receipt API + +extension StoreKitManager { + /// This refreshes the device receipt. + /// + /// - Warning: This will prompt the user to log in, so only do this on + /// when restoring or after purchasing. + @discardableResult + nonisolated func refreshReceipt() async -> Bool { + Logger.debug( + logLevel: .debug, + scope: .storeKitManager, + message: "Refreshing App Store receipt." + ) + return await receiptManager.refreshReceipt() + } + + /// Loads the purchased products from the receipt, + nonisolated func loadPurchasedProducts() async { + Logger.debug( + logLevel: .debug, + scope: .storeKitManager, + message: "Loading purchased products from the App Store receipt." + ) + await receiptManager.loadPurchasedProducts() + } + + /// Determines whether a free trial is available based on the product the user is purchasing. + /// + /// A free trial is available if the user hasn't already purchased within the subscription group of the + /// supplied product. If it isn't a subscription-based product or there are other issues retrieving the products, + /// the outcome will default to whether or not the user has already purchased that product. + nonisolated func isFreeTrialAvailable(for product: StoreProduct) -> Bool { + return receiptManager.isFreeTrialAvailable(for: product) + } +} + // MARK: - ProductsFetcher extension StoreKitManager: ProductsFetcher { - func products(identifiers: Set) async throws -> Set { + nonisolated func products(identifiers: Set) async throws -> Set { return try await coordinator.productFetcher.products(identifiers: identifiers) } } @@ -157,7 +161,7 @@ extension StoreKitManager: ProductsFetcher { // MARK: - SubscriptionStatusChecker extension StoreKitManager: SubscriptionStatusChecker { /// Do not call this directly. - func isSubscribed() -> Bool { + nonisolated func isSubscribed() -> Bool { return !receiptManager.activePurchases.isEmpty } } diff --git a/Sources/SuperwallKit/StoreKit/Transactions/Purchasing/ProductPurchaserSK1.swift b/Sources/SuperwallKit/StoreKit/Transactions/Purchasing/ProductPurchaserSK1.swift index ded58fa8e..fb2291e3a 100644 --- a/Sources/SuperwallKit/StoreKit/Transactions/Purchasing/ProductPurchaserSK1.swift +++ b/Sources/SuperwallKit/StoreKit/Transactions/Purchasing/ProductPurchaserSK1.swift @@ -259,7 +259,7 @@ extension ProductPurchaserSK1: SKPaymentTransactionObserver { _ transaction: SKPaymentTransaction, isPaywallPresented: Bool ) async { - guard let product = storeKitManager.productsById[transaction.payment.productIdentifier] else { + guard let product = await storeKitManager.productsById[transaction.payment.productIdentifier] else { return } guard isPaywallPresented else { diff --git a/Sources/SuperwallKit/StoreKit/Transactions/TransactionManager.swift b/Sources/SuperwallKit/StoreKit/Transactions/TransactionManager.swift index baeac17b6..e9acf27f6 100644 --- a/Sources/SuperwallKit/StoreKit/Transactions/TransactionManager.swift +++ b/Sources/SuperwallKit/StoreKit/Transactions/TransactionManager.swift @@ -36,7 +36,7 @@ final class TransactionManager { _ productId: String, from paywallViewController: PaywallViewController ) async { - guard let product = storeKitManager.productsById[productId] else { + guard let product = await storeKitManager.productsById[productId] else { return } diff --git a/Sources/SuperwallKit/Superwall.swift b/Sources/SuperwallKit/Superwall.swift index 3aebf0b2b..4970d26ba 100644 --- a/Sources/SuperwallKit/Superwall.swift +++ b/Sources/SuperwallKit/Superwall.swift @@ -1,15 +1,17 @@ +// swiftlint:disable file_length + import Foundation import StoreKit import Combine /// The primary class for integrating Superwall into your application. After configuring via -/// ``configure(apiKey:delegate:options:)-65jyx``, It provides access to +/// ``configure(apiKey:delegate:options:completion:)-7fafw``, It provides access to /// all its featured via instance functions and variables. @objcMembers public final class Superwall: NSObject, ObservableObject { // MARK: - Public Properties /// The optional purchasing delegate of the Superwall instance. Set this in - /// ``configure(apiKey:delegate:options:)-65jyx`` + /// ``configure(apiKey:delegate:options:completion:)-7fafw`` /// when you want to manually handle the purchasing logic within your app. public var delegate: SuperwallDelegate? { get { @@ -22,7 +24,7 @@ public final class Superwall: NSObject, ObservableObject { } /// The optional purchasing delegate of the Superwall instance. Set this in - /// ``configure(apiKey:delegate:options:)-65jyx`` + /// ``configure(apiKey:delegate:options:completion:)-7fafw`` /// when you want to manually handle the purchasing logic within your app. @available(swift, obsoleted: 1.0) @objc(delegate) @@ -58,7 +60,7 @@ public final class Superwall: NSObject, ObservableObject { } /// A convenience variable to access and change the paywall options that you passed - /// to ``configure(apiKey:delegate:options:)-65jyx``. + /// to ``configure(apiKey:delegate:options:completion:)-7fafw``. public var options: SuperwallOptions { return dependencyContainer.configManager.options } @@ -72,7 +74,7 @@ public final class Superwall: NSObject, ObservableObject { /// The current user's id. /// - /// If you haven't called ``logIn(userId:)`` or ``createAccount(userId:)``, + /// If you haven't called ``identify(userId:options:)``, /// this value will return an anonymous user id which is cached to disk public var userId: String { return dependencyContainer.identityManager.userId @@ -80,8 +82,8 @@ public final class Superwall: NSObject, ObservableObject { /// Indicates whether the user is logged in to Superwall. /// - /// If you have previously called ``logIn(userId:)`` or - /// ``createAccount(userId:)``, this will return true. + /// If you have previously called ``identify(userId:options:)``, this will + /// return `true`. /// /// - Returns: A boolean indicating whether the user is logged in or not. public var isLoggedIn: Bool { @@ -95,26 +97,37 @@ public final class Superwall: NSObject, ObservableObject { /// If you're using Combine or SwiftUI, you can subscribe or bind to this to get /// notified whenever the user's subscription status changes. /// + /// Otherwise, you can check the delegate function + /// ``SuperwallDelegate/hasActiveSubscriptionDidChange(to:)-1rmfo`` + /// to receive a callback with the new value every time it changes. + /// /// If you are returning a ``SubscriptionController`` in the /// ``SuperwallDelegate``, you should rely on your own subscription status instead. @Published public var hasActiveSubscription = false { didSet { dependencyContainer.storage.save(hasActiveSubscription, forType: SubscriptionStatus.self) + if oldValue == hasActiveSubscription { + return + } + dependencyContainer.delegateAdapter.hasActiveSubscriptionDidChange(to: hasActiveSubscription) } } /// A published property that is `true` when Superwall has finished configuring via - /// ``configure(apiKey:delegate:options:)-65jyx``. + /// ``configure(apiKey:delegate:options:completion:)-7fafw``. /// /// If you're using Combine or SwiftUI, you can subscribe or bind to this to get /// notified when configuration has completed. + /// + /// Alternatively, you can use the completion handler from + /// ``configure(apiKey:delegate:options:completion:)-7fafw``. @Published public var isConfigured = false /// The configured shared instance of ``Superwall``. /// - /// - Warning: You must call ``configure(apiKey:delegate:options:)-65jyx`` + /// - Warning: You must call ``configure(apiKey:delegate:options:completion:)-7fafw`` /// to initialize ``Superwall`` before using this. @objc(sharedInstance) public static var shared: Superwall { @@ -170,7 +183,8 @@ public final class Superwall: NSObject, ObservableObject { apiKey: String, swiftDelegate: SuperwallDelegate? = nil, objcDelegate: SuperwallDelegateObjc? = nil, - options: SuperwallOptions? = nil + options: SuperwallOptions? = nil, + completion: (() -> Void)? ) { let dependencyContainer = DependencyContainer( swiftDelegate: swiftDelegate, @@ -193,6 +207,10 @@ public final class Superwall: NSObject, ObservableObject { await dependencyContainer.configManager.fetchConfiguration() await dependencyContainer.identityManager.configure() + + await MainActor.run { + completion?() + } } } @@ -221,14 +239,17 @@ public final class Superwall: NSObject, ObservableObject { /// an account, you can [sign up for free](https://superwall.com/sign-up). /// - delegate: An optional class that conforms to ``SuperwallDelegate``. The delegate methods receive /// callbacks from the SDK in response to certain events on the paywall. - /// - options: A ``SuperwallOptions`` object which allows you to customise the appearance and behavior + /// - options: An optional ``SuperwallOptions`` object which allows you to customise the appearance and behavior /// of the paywall. + /// - completion: An optional completion handler that lets you know when Superwall has finished configuring. + /// Alternatively, you can subscribe to the published variable ``isConfigured``. /// - Returns: The newly configured ``Superwall`` instance. @discardableResult public static func configure( apiKey: String, delegate: SuperwallDelegate? = nil, - options: SuperwallOptions? = nil + options: SuperwallOptions? = nil, + completion: (() -> Void)? = nil ) -> Superwall { guard superwall == nil else { Logger.debug( @@ -242,7 +263,8 @@ public final class Superwall: NSObject, ObservableObject { apiKey: apiKey, swiftDelegate: delegate, objcDelegate: nil, - options: options + options: options, + completion: completion ) return shared } @@ -254,13 +276,15 @@ public final class Superwall: NSObject, ObservableObject { /// - apiKey: Your Public API Key that you can get from the Superwall dashboard settings. If you don't have an account, you can [sign up for free](https://superwall.com/sign-up). /// - delegate: An optional class that conforms to ``SuperwallDelegate``. The delegate methods receive callbacks from the SDK in response to certain events on the paywall. /// - options: A ``SuperwallOptions`` object which allows you to customise the appearance and behavior of the paywall. + /// - completion: An optional completion handler that lets you know when Superwall has finished configuring. /// - Returns: The newly configured ``SuperwallKit/Superwall`` instance. @discardableResult @available(swift, obsoleted: 1.0) public static func configure( apiKey: String, delegate: SuperwallDelegateObjc? = nil, - options: SuperwallOptions? = nil + options: SuperwallOptions? = nil, + completion: (() -> Void)? = nil ) -> Superwall { guard superwall == nil else { Logger.debug( @@ -274,7 +298,8 @@ public final class Superwall: NSObject, ObservableObject { apiKey: apiKey, swiftDelegate: nil, objcDelegate: delegate, - options: options + options: options, + completion: completion ) return shared } @@ -307,11 +332,14 @@ public final class Superwall: NSObject, ObservableObject { /// Handles a deep link sent to your app to open a preview of your paywall. /// /// You can preview your paywall on-device before going live by utilizing paywall previews. This uses a deep link to render a preview of a paywall you've configured on the Superwall dashboard on your device. See for more. - public func handleDeepLink(_ url: URL) { + /// + /// - Returns: A `Bool` that is `true` if the deep link was handled. + @discardableResult + public func handleDeepLink(_ url: URL) -> Bool { Task { await track(InternalSuperwallEvent.DeepLink(url: url)) - await dependencyContainer.debugManager.handle(deepLinkUrl: url) } + return dependencyContainer.debugManager.handle(deepLinkUrl: url) } // MARK: - Overrides @@ -323,6 +351,19 @@ public final class Superwall: NSObject, ObservableObject { public func localizationOverride(localeIdentifier: String? = nil) { dependencyContainer.localizationManager.selectedLocale = localeIdentifier } + + /// Toggles the paywall loading spinner on and off. + /// + /// Use this when you want to do asynchronous work inside + /// ``SuperwallDelegate/handleCustomPaywallAction(withName:)-b8fk``. + public func togglePaywallSpinner(isHidden: Bool) { + Task { @MainActor in + guard let paywallViewController = dependencyContainer.paywallManager.presentedViewController else { + return + } + paywallViewController.togglePaywallSpinner(isHidden: isHidden) + } + } } // MARK: - PaywallViewControllerDelegate diff --git a/SuperwallKit.podspec b/SuperwallKit.podspec index 4a152adbf..15505c51f 100644 --- a/SuperwallKit.podspec +++ b/SuperwallKit.podspec @@ -1,11 +1,11 @@ Pod::Spec.new do |s| s.name = "SuperwallKit" - s.version = "3.0.0-beta.3" + s.version = "3.0.0-beta.4" s.summary = "Superwall: In-App Paywalls Made Easy" s.description = "Paywall infrastructure for mobile apps :) we make things like editing your paywall and running price tests as easy as clicking a few buttons. superwall.com" - s.homepage = "https://github.com/superwall-me/SuperwallKit-iOS" + s.homepage = "https://github.com/superwall-me/Superwall-iOS" s.license = { :type => 'MIT', :text => <<-LICENSE MIT License @@ -30,7 +30,7 @@ Pod::Spec.new do |s| SOFTWARE. LICENSE } - s.source = { :git => "https://github.com/superwall-me/SuperwallKit-iOS.git", :tag => "#{s.version}" } + s.source = { :git => "https://github.com/superwall-me/Superwall-iOS.git", :tag => "#{s.version}" } s.author = { "Jake Mor" => "jake@superwall.com" } s.documentation_url = "https://docs.superwall.com/" diff --git a/Tests/SuperwallKitTests/Analytics/Trigger Session Manager/TriggerSessionManagerTests.swift b/Tests/SuperwallKitTests/Analytics/Trigger Session Manager/TriggerSessionManagerTests.swift index fea1cefc9..754d4e7b4 100644 --- a/Tests/SuperwallKitTests/Analytics/Trigger Session Manager/TriggerSessionManagerTests.swift +++ b/Tests/SuperwallKitTests/Analytics/Trigger Session Manager/TriggerSessionManagerTests.swift @@ -817,11 +817,11 @@ final class TriggerSessionManagerTests: XCTestCase { await NotificationCenter.default.post(Notification(name: UIApplication.didEnterBackgroundNotification)) // Then - try? await Task.sleep(nanoseconds: 100_000_000) + try? await Task.sleep(nanoseconds: 500_000_000) let triggerSessions2 = await queue.triggerSessions - try? await Task.sleep(nanoseconds: 100_000_000) + try? await Task.sleep(nanoseconds: 500_000_000) XCTAssertEqual(triggerSessions2.count, 1) XCTAssertEqual(triggerSessions2.last?.id, lastTriggerSession.id) XCTAssertNotNil(triggerSessions2.last!.endAt) diff --git a/Tests/SuperwallKitTests/Config/Assignments/Expression Evaluator/ExpressionEvaluatorTests.swift b/Tests/SuperwallKitTests/Config/Assignments/Expression Evaluator/ExpressionEvaluatorTests.swift index 10cfd0ff0..e7638c19f 100644 --- a/Tests/SuperwallKitTests/Config/Assignments/Expression Evaluator/ExpressionEvaluatorTests.swift +++ b/Tests/SuperwallKitTests/Config/Assignments/Expression Evaluator/ExpressionEvaluatorTests.swift @@ -11,9 +11,9 @@ import XCTest @testable import SuperwallKit final class ExpressionEvaluatorTests: XCTestCase { - func testExpressionMatchesAll() { + func testExpressionMatchesAll() async { let dependencyContainer = DependencyContainer() - dependencyContainer.storage.reset() + await dependencyContainer.storage.reset() let evaluator = ExpressionEvaluator( storage: dependencyContainer.storage, factory: dependencyContainer @@ -30,9 +30,9 @@ final class ExpressionEvaluatorTests: XCTestCase { // MARK: - Expression - func testExpressionEvaluator_expressionTrue() { + func testExpressionEvaluator_expressionTrue() async { let dependencyContainer = DependencyContainer() - dependencyContainer.storage.reset() + await dependencyContainer.storage.reset() let evaluator = ExpressionEvaluator( storage: dependencyContainer.storage, factory: dependencyContainer @@ -47,9 +47,9 @@ final class ExpressionEvaluatorTests: XCTestCase { XCTAssertTrue(result) } - func testExpressionEvaluator_expressionParams() { + func testExpressionEvaluator_expressionParams() async { let dependencyContainer = DependencyContainer() - dependencyContainer.storage.reset() + await dependencyContainer.storage.reset() let evaluator = ExpressionEvaluator( storage: dependencyContainer.storage, factory: dependencyContainer @@ -64,9 +64,9 @@ final class ExpressionEvaluatorTests: XCTestCase { XCTAssertTrue(result) } - func testExpressionEvaluator_expressionDeviceTrue() { + func testExpressionEvaluator_expressionDeviceTrue() async { let dependencyContainer = DependencyContainer() - dependencyContainer.storage.reset() + await dependencyContainer.storage.reset() let evaluator = ExpressionEvaluator( storage: dependencyContainer.storage, factory: dependencyContainer @@ -81,9 +81,9 @@ final class ExpressionEvaluatorTests: XCTestCase { XCTAssertTrue(result) } - func testExpressionEvaluator_expressionDeviceFalse() { + func testExpressionEvaluator_expressionDeviceFalse() async { let dependencyContainer = DependencyContainer() - dependencyContainer.storage.reset() + await dependencyContainer.storage.reset() let evaluator = ExpressionEvaluator( storage: dependencyContainer.storage, factory: dependencyContainer @@ -98,9 +98,9 @@ final class ExpressionEvaluatorTests: XCTestCase { XCTAssertFalse(result) } - func testExpressionEvaluator_expressionFalse() { + func testExpressionEvaluator_expressionFalse() async { let dependencyContainer = DependencyContainer() - dependencyContainer.storage.reset() + await dependencyContainer.storage.reset() let evaluator = ExpressionEvaluator( storage: dependencyContainer.storage, factory: dependencyContainer diff --git a/Tests/SuperwallKitTests/Config/ConfigManagerTests.swift b/Tests/SuperwallKitTests/Config/ConfigManagerTests.swift index d9ad2da9a..cfaf75c8c 100644 --- a/Tests/SuperwallKitTests/Config/ConfigManagerTests.swift +++ b/Tests/SuperwallKitTests/Config/ConfigManagerTests.swift @@ -57,7 +57,15 @@ final class ConfigManagerTests: XCTestCase { factory: dependencyContainer ) - await configManager.getAssignments() + let expectation = expectation(description: "No assignments") + expectation.isInverted = true + Task { + await configManager.getAssignments() + expectation.fulfill() + } + + await waitForExpectations(timeout: 1) + XCTAssertTrue(storage.getConfirmedAssignments().isEmpty) XCTAssertTrue(configManager.unconfirmedAssignments.isEmpty) diff --git a/Tests/SuperwallKitTests/Paywall Manager/PaywallCacheLogicTests.swift b/Tests/SuperwallKitTests/Paywall/Manager/PaywallCacheLogicTests.swift similarity index 100% rename from Tests/SuperwallKitTests/Paywall Manager/PaywallCacheLogicTests.swift rename to Tests/SuperwallKitTests/Paywall/Manager/PaywallCacheLogicTests.swift diff --git a/Tests/SuperwallKitTests/Paywall Manager/PaywallCacheTests.swift b/Tests/SuperwallKitTests/Paywall/Manager/PaywallCacheTests.swift similarity index 100% rename from Tests/SuperwallKitTests/Paywall Manager/PaywallCacheTests.swift rename to Tests/SuperwallKitTests/Paywall/Manager/PaywallCacheTests.swift diff --git a/Tests/SuperwallKitTests/Paywall Manager/PaywallManagerMock.swift b/Tests/SuperwallKitTests/Paywall/Manager/PaywallManagerMock.swift similarity index 100% rename from Tests/SuperwallKitTests/Paywall Manager/PaywallManagerMock.swift rename to Tests/SuperwallKitTests/Paywall/Manager/PaywallManagerMock.swift diff --git a/Tests/SuperwallKitTests/Paywall Presentation/Get Track Result/Operators/CheckForPaywallResultTests.swift b/Tests/SuperwallKitTests/Paywall/Presentation/Get Track Result/Operators/CheckForPaywallResultTests.swift similarity index 100% rename from Tests/SuperwallKitTests/Paywall Presentation/Get Track Result/Operators/CheckForPaywallResultTests.swift rename to Tests/SuperwallKitTests/Paywall/Presentation/Get Track Result/Operators/CheckForPaywallResultTests.swift diff --git a/Tests/SuperwallKitTests/Paywall Presentation/Get Track Result/Operators/CheckPaywallIsPresentableTests.swift b/Tests/SuperwallKitTests/Paywall/Presentation/Get Track Result/Operators/CheckPaywallIsPresentableTests.swift similarity index 100% rename from Tests/SuperwallKitTests/Paywall Presentation/Get Track Result/Operators/CheckPaywallIsPresentableTests.swift rename to Tests/SuperwallKitTests/Paywall/Presentation/Get Track Result/Operators/CheckPaywallIsPresentableTests.swift diff --git a/Tests/SuperwallKitTests/Paywall Presentation/Get Track Result/Operators/GetPaywallViewControllerNoChecksTests.swift b/Tests/SuperwallKitTests/Paywall/Presentation/Get Track Result/Operators/GetPaywallViewControllerNoChecksTests.swift similarity index 100% rename from Tests/SuperwallKitTests/Paywall Presentation/Get Track Result/Operators/GetPaywallViewControllerNoChecksTests.swift rename to Tests/SuperwallKitTests/Paywall/Presentation/Get Track Result/Operators/GetPaywallViewControllerNoChecksTests.swift diff --git a/Tests/SuperwallKitTests/Paywall Presentation/Internal Presentation/InternalPresentationLogicTests.swift b/Tests/SuperwallKitTests/Paywall/Presentation/Internal Presentation/InternalPresentationLogicTests.swift similarity index 100% rename from Tests/SuperwallKitTests/Paywall Presentation/Internal Presentation/InternalPresentationLogicTests.swift rename to Tests/SuperwallKitTests/Paywall/Presentation/Internal Presentation/InternalPresentationLogicTests.swift diff --git a/Tests/SuperwallKitTests/Paywall Presentation/Internal Presentation/Operators/AwaitIdentityOperatorTests.swift b/Tests/SuperwallKitTests/Paywall/Presentation/Internal Presentation/Operators/AwaitIdentityOperatorTests.swift similarity index 100% rename from Tests/SuperwallKitTests/Paywall Presentation/Internal Presentation/Operators/AwaitIdentityOperatorTests.swift rename to Tests/SuperwallKitTests/Paywall/Presentation/Internal Presentation/Operators/AwaitIdentityOperatorTests.swift diff --git a/Tests/SuperwallKitTests/Paywall Presentation/Internal Presentation/Operators/CheckDebuggerPresentationOperatorTests.swift b/Tests/SuperwallKitTests/Paywall/Presentation/Internal Presentation/Operators/CheckDebuggerPresentationOperatorTests.swift similarity index 100% rename from Tests/SuperwallKitTests/Paywall Presentation/Internal Presentation/Operators/CheckDebuggerPresentationOperatorTests.swift rename to Tests/SuperwallKitTests/Paywall/Presentation/Internal Presentation/Operators/CheckDebuggerPresentationOperatorTests.swift diff --git a/Tests/SuperwallKitTests/Paywall Presentation/Internal Presentation/Operators/CheckPaywallPresentableOperatorTests.swift b/Tests/SuperwallKitTests/Paywall/Presentation/Internal Presentation/Operators/CheckPaywallPresentableOperatorTests.swift similarity index 100% rename from Tests/SuperwallKitTests/Paywall Presentation/Internal Presentation/Operators/CheckPaywallPresentableOperatorTests.swift rename to Tests/SuperwallKitTests/Paywall/Presentation/Internal Presentation/Operators/CheckPaywallPresentableOperatorTests.swift diff --git a/Tests/SuperwallKitTests/Paywall Presentation/Internal Presentation/Operators/CheckUserSubscriptionOperatorTests.swift b/Tests/SuperwallKitTests/Paywall/Presentation/Internal Presentation/Operators/CheckUserSubscriptionOperatorTests.swift similarity index 100% rename from Tests/SuperwallKitTests/Paywall Presentation/Internal Presentation/Operators/CheckUserSubscriptionOperatorTests.swift rename to Tests/SuperwallKitTests/Paywall/Presentation/Internal Presentation/Operators/CheckUserSubscriptionOperatorTests.swift diff --git a/Tests/SuperwallKitTests/Paywall Presentation/Internal Presentation/Operators/ConfirmHoldoutAssignment.swift b/Tests/SuperwallKitTests/Paywall/Presentation/Internal Presentation/Operators/ConfirmHoldoutAssignment.swift similarity index 100% rename from Tests/SuperwallKitTests/Paywall Presentation/Internal Presentation/Operators/ConfirmHoldoutAssignment.swift rename to Tests/SuperwallKitTests/Paywall/Presentation/Internal Presentation/Operators/ConfirmHoldoutAssignment.swift diff --git a/Tests/SuperwallKitTests/Paywall Presentation/Internal Presentation/Operators/ConfirmPaywallAssignmentOperatorTests.swift b/Tests/SuperwallKitTests/Paywall/Presentation/Internal Presentation/Operators/ConfirmPaywallAssignmentOperatorTests.swift similarity index 100% rename from Tests/SuperwallKitTests/Paywall Presentation/Internal Presentation/Operators/ConfirmPaywallAssignmentOperatorTests.swift rename to Tests/SuperwallKitTests/Paywall/Presentation/Internal Presentation/Operators/ConfirmPaywallAssignmentOperatorTests.swift diff --git a/Tests/SuperwallKitTests/Paywall Presentation/Internal Presentation/Operators/EvaluateRulesOperatorTests.swift b/Tests/SuperwallKitTests/Paywall/Presentation/Internal Presentation/Operators/EvaluateRulesOperatorTests.swift similarity index 100% rename from Tests/SuperwallKitTests/Paywall Presentation/Internal Presentation/Operators/EvaluateRulesOperatorTests.swift rename to Tests/SuperwallKitTests/Paywall/Presentation/Internal Presentation/Operators/EvaluateRulesOperatorTests.swift diff --git a/Tests/SuperwallKitTests/Paywall Presentation/Internal Presentation/Operators/GetPaywallVcOperator.swift b/Tests/SuperwallKitTests/Paywall/Presentation/Internal Presentation/Operators/GetPaywallVcOperator.swift similarity index 100% rename from Tests/SuperwallKitTests/Paywall Presentation/Internal Presentation/Operators/GetPaywallVcOperator.swift rename to Tests/SuperwallKitTests/Paywall/Presentation/Internal Presentation/Operators/GetPaywallVcOperator.swift diff --git a/Tests/SuperwallKitTests/Paywall Presentation/Internal Presentation/Operators/HandleTriggerResultOperatorTests.swift b/Tests/SuperwallKitTests/Paywall/Presentation/Internal Presentation/Operators/HandleTriggerResultOperatorTests.swift similarity index 100% rename from Tests/SuperwallKitTests/Paywall Presentation/Internal Presentation/Operators/HandleTriggerResultOperatorTests.swift rename to Tests/SuperwallKitTests/Paywall/Presentation/Internal Presentation/Operators/HandleTriggerResultOperatorTests.swift diff --git a/Tests/SuperwallKitTests/Paywall Presentation/Internal Presentation/Operators/PresentPaywallOperatorTests.swift b/Tests/SuperwallKitTests/Paywall/Presentation/Internal Presentation/Operators/PresentPaywallOperatorTests.swift similarity index 100% rename from Tests/SuperwallKitTests/Paywall Presentation/Internal Presentation/Operators/PresentPaywallOperatorTests.swift rename to Tests/SuperwallKitTests/Paywall/Presentation/Internal Presentation/Operators/PresentPaywallOperatorTests.swift diff --git a/Tests/SuperwallKitTests/Paywall Presentation/Internal Presentation/Operators/StorePresentationObjectsOperatorTests.swift b/Tests/SuperwallKitTests/Paywall/Presentation/Internal Presentation/Operators/StorePresentationObjectsOperatorTests.swift similarity index 100% rename from Tests/SuperwallKitTests/Paywall Presentation/Internal Presentation/Operators/StorePresentationObjectsOperatorTests.swift rename to Tests/SuperwallKitTests/Paywall/Presentation/Internal Presentation/Operators/StorePresentationObjectsOperatorTests.swift diff --git a/Tests/SuperwallKitTests/Paywall Request/Mocks/MockIntroductoryPeriod.swift b/Tests/SuperwallKitTests/Paywall/Request/Mocks/MockIntroductoryPeriod.swift similarity index 100% rename from Tests/SuperwallKitTests/Paywall Request/Mocks/MockIntroductoryPeriod.swift rename to Tests/SuperwallKitTests/Paywall/Request/Mocks/MockIntroductoryPeriod.swift diff --git a/Tests/SuperwallKitTests/Paywall Request/Mocks/MockSubscriptionPeriod.swift b/Tests/SuperwallKitTests/Paywall/Request/Mocks/MockSubscriptionPeriod.swift similarity index 100% rename from Tests/SuperwallKitTests/Paywall Request/Mocks/MockSubscriptionPeriod.swift rename to Tests/SuperwallKitTests/Paywall/Request/Mocks/MockSubscriptionPeriod.swift diff --git a/Tests/SuperwallKitTests/Paywall Request/PaywallLogicTests.swift b/Tests/SuperwallKitTests/Paywall/Request/PaywallLogicTests.swift similarity index 100% rename from Tests/SuperwallKitTests/Paywall Request/PaywallLogicTests.swift rename to Tests/SuperwallKitTests/Paywall/Request/PaywallLogicTests.swift diff --git a/Tests/SuperwallKitTests/Paywall View Controller/PaywallViewControllerMock.swift b/Tests/SuperwallKitTests/Paywall/View Controller/PaywallViewControllerMock.swift similarity index 100% rename from Tests/SuperwallKitTests/Paywall View Controller/PaywallViewControllerMock.swift rename to Tests/SuperwallKitTests/Paywall/View Controller/PaywallViewControllerMock.swift diff --git a/Tests/SuperwallKitTests/Paywall View Controller/Web View/Message Handling/PaywallMessageHandlerDelegateMock.swift b/Tests/SuperwallKitTests/Paywall/View Controller/Web View/Message Handling/PaywallMessageHandlerDelegateMock.swift similarity index 94% rename from Tests/SuperwallKitTests/Paywall View Controller/Web View/Message Handling/PaywallMessageHandlerDelegateMock.swift rename to Tests/SuperwallKitTests/Paywall/View Controller/Web View/Message Handling/PaywallMessageHandlerDelegateMock.swift index dd7dc007e..c0dc97ae9 100644 --- a/Tests/SuperwallKitTests/Paywall View Controller/Web View/Message Handling/PaywallMessageHandlerDelegateMock.swift +++ b/Tests/SuperwallKitTests/Paywall/View Controller/Web View/Message Handling/PaywallMessageHandlerDelegateMock.swift @@ -29,7 +29,7 @@ final class PaywallMessageHandlerDelegateMock: PaywallMessageHandlerDelegate { var webView: SuperwallKit.SWWebView - var loadingState: SuperwallKit.PaywallLoadingState = .loadingResponse + var loadingState: SuperwallKit.PaywallLoadingState = .loadingURL var isActive = false diff --git a/Tests/SuperwallKitTests/Paywall View Controller/Web View/Message Handling/PaywallMessageHandlerTests.swift b/Tests/SuperwallKitTests/Paywall/View Controller/Web View/Message Handling/PaywallMessageHandlerTests.swift similarity index 100% rename from Tests/SuperwallKitTests/Paywall View Controller/Web View/Message Handling/PaywallMessageHandlerTests.swift rename to Tests/SuperwallKitTests/Paywall/View Controller/Web View/Message Handling/PaywallMessageHandlerTests.swift diff --git a/Tests/SuperwallKitTests/Paywall View Controller/Web View/Message Handling/RawWebMessageHandlerTests.swift b/Tests/SuperwallKitTests/Paywall/View Controller/Web View/Message Handling/RawWebMessageHandlerTests.swift similarity index 100% rename from Tests/SuperwallKitTests/Paywall View Controller/Web View/Message Handling/RawWebMessageHandlerTests.swift rename to Tests/SuperwallKitTests/Paywall/View Controller/Web View/Message Handling/RawWebMessageHandlerTests.swift diff --git a/Tests/SuperwallKitTests/Paywall View Controller/Web View/Templating/TemplateLogicTests.swift b/Tests/SuperwallKitTests/Paywall/View Controller/Web View/Templating/TemplateLogicTests.swift similarity index 100% rename from Tests/SuperwallKitTests/Paywall View Controller/Web View/Templating/TemplateLogicTests.swift rename to Tests/SuperwallKitTests/Paywall/View Controller/Web View/Templating/TemplateLogicTests.swift diff --git a/Tests/SuperwallKitTests/Storage/Core Data/CoreDataManagerTests.swift b/Tests/SuperwallKitTests/Storage/Core Data/CoreDataManagerTests.swift index 522f62be1..32630a17e 100644 --- a/Tests/SuperwallKitTests/Storage/Core Data/CoreDataManagerTests.swift +++ b/Tests/SuperwallKitTests/Storage/Core Data/CoreDataManagerTests.swift @@ -73,7 +73,7 @@ class CoreDataManagerTests: XCTestCase { } // MARK: - Delete All Entities - func test_deleteAllEntities() { + func test_deleteAllEntities() async { // Save Event Data with Params let eventName = "abc" let eventData: EventData = .stub() @@ -92,7 +92,7 @@ class CoreDataManagerTests: XCTestCase { expectation1.fulfill() } - waitForExpectations(timeout: 2.0) { error in + await waitForExpectations(timeout: 2.0) { error in XCTAssertNil(error, "Save did not occur") } @@ -115,19 +115,12 @@ class CoreDataManagerTests: XCTestCase { expectation2.fulfill() } - waitForExpectations(timeout: 20.0) { error in + await waitForExpectations(timeout: 20.0) { error in XCTAssertNil(error, "Save did not occur") } // Delete All Entities - let expectation3 = expectation(description: "Delete entities") - coreDataManager.deleteAllEntities() { - expectation3.fulfill() - } - - waitForExpectations(timeout: 2.0) { error in - XCTAssertNil(error, "Save did not occur") - } + await coreDataManager.deleteAllEntities() // Count triggers let occurrenceCount = coreDataManager.countTriggerRuleOccurrences(for: occurrence) diff --git a/Tests/SuperwallKitTests/Storage/Core Data/CoreDataStackMock.swift b/Tests/SuperwallKitTests/Storage/Core Data/CoreDataStackMock.swift index b0f037d6b..536fe5e3b 100644 --- a/Tests/SuperwallKitTests/Storage/Core Data/CoreDataStackMock.swift +++ b/Tests/SuperwallKitTests/Storage/Core Data/CoreDataStackMock.swift @@ -11,12 +11,13 @@ import CoreData @available(iOS 14.0, *) final class CoreDataStackMock: CoreDataStack { - func deleteAllEntities(named entityName: String) { + func deleteAllEntities(named entityName: String, completion: () -> Void) { let fetchRequest: NSFetchRequest = NSFetchRequest(entityName: entityName) let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest) do { try mainContext.executeAndMergeChanges(using: deleteRequest) + completion() print("Deleted entities") } catch let error as NSError { print("Error deleting!", error) diff --git a/Tests/SuperwallKitTests/StoreKit/Products/ProductsManagerMock.swift b/Tests/SuperwallKitTests/StoreKit/Products/ProductsFetcherSK1.swift similarity index 100% rename from Tests/SuperwallKitTests/StoreKit/Products/ProductsManagerMock.swift rename to Tests/SuperwallKitTests/StoreKit/Products/ProductsFetcherSK1.swift