Skip to content

Commit

Permalink
Merge pull request #174 from superwall-me/develop
Browse files Browse the repository at this point in the history
v3.4.0
  • Loading branch information
yusuftor committed Sep 19, 2023
2 parents 57ef30f + 1c32301 commit c920dc0
Show file tree
Hide file tree
Showing 90 changed files with 1,964 additions and 1,301 deletions.
8 changes: 5 additions & 3 deletions .github/PULL_REQUEST_TEMPLATE.md
@@ -1,11 +1,13 @@
## Changes in this pull request

Issue fixed: #
-

### Checklist

- [ ] All unit and UI tests pass. Demo project builds and runs.
- [ ] I added tests, an experiment, or detailed why my change isn't tested.
- [ ] All unit tests pass.
- [ ] All UI tests pass.
- [ ] Demo project builds and runs.
- [ ] I added/updated tests or detailed why my change isn't tested.
- [ ] I added an entry to the `CHANGELOG.md` for any breaking changes, enhancements, or bug fixes.
- [ ] I have run `swiftlint` in the main directory and fixed any issues.
- [ ] I have updated the SDK documentation as well as the online docs.
Expand Down
20 changes: 19 additions & 1 deletion CHANGELOG.md
Expand Up @@ -2,6 +2,24 @@

The changelog for `SuperwallKit`. Also see the [releases](https://github.com/superwall-me/Superwall-iOS/releases) on GitHub.

## 3.4.0

### Enhancements

- Adds `sdk_version`, `sdk_version_padded`, `app_build_string`, and `app_build_string_number` to the device object for use in rules. `sdk_version` is the version of the sdk, e.g. `3.4.0`. `sdk_version_padded` is the sdk version padded with zeros for use with string comparison. For example `003.004.000`. `app_build_string` is the build of your app and `app_build_string_number` is the build of your app casted as an Int.
- When you experience `no_rule_match`, the `TriggerFire` event params will specify which part of the rules didn't match in the format `"unmatched_rule_<id>": "<outcome>"`. Where `outcome` will either be `OCCURRENCE`, referring to the limit applied to a rule, or `EXPRESSION`. The `id` is the experiment id.
- Adds a `touches_began` implicit trigger. By adding the `touches_began` event to a campaign, you can show a paywall the first time a user touches anywhere in your app.
- Adds the ability to include a close button on a survey.
- If running in sandbox, the duration of a free trial notification added to a paywall will be converted from days to minutes for testing purposes.
- Adds the ability to show a survey after purchasing a product.

### Fixes

- Fixes issue where a survey attached to a paywall wouldn't show if you were also using the `paywall_decline` trigger.
- Fixes issue where verification was happening after the finishing of transactions when not using a `PurchaseController`.
- Fixes issue where the retrieved `StoreTransaction` associated with the purchased product may be `nil`.
- Fixes issue where a `presentationRequest` wasn't being tracked for implicit triggers like `session_start` when there was no internet.

## 3.3.2

### Fixes
Expand All @@ -27,7 +45,7 @@ The changelog for `SuperwallKit`. Also see the [releases](https://github.com/sup
### Enhancements

- Adds the ability to add a paywall exit survey. Surveys are configured via the dashboard and added to paywalls. When added to a paywall, it will attempt to display when the user taps the close button. If the paywall has the `modalPresentationStyle` of `pageSheet`, `formSheet`, or `popover`, the survey will also attempt to display when the user tries to drag to dismiss the paywall. The probability of the survey showing is determined by the survey's configuration in the dashboard. A user will only ever see the survey once unless you reset responses via the dashboard. The survey will always show on exit of the paywall in the debugger.
- Adds the ability to add `survey_close` as a trigger and use the selected option title in rules.
- Adds the ability to add `survey_response` as a trigger and use the selected option title in rules.
- Adds new `PaywallCloseReason` `.manualClose`.

### Fixes
Expand Down
Expand Up @@ -35,16 +35,28 @@ enum InternalSuperwallEvent {
struct AppInstall: TrackableSuperwallEvent {
let superwallEvent: SuperwallEvent = .appInstall
let appInstalledAtString: String
let hasPurchaseController: Bool
let hasExternalPurchaseController: Bool
var customParameters: [String: Any] = [:]
func getSuperwallParameters() async -> [String: Any] {
return [
"application_installed_at": appInstalledAtString,
"using_purchase_controller": hasPurchaseController
"using_purchase_controller": hasExternalPurchaseController
]
}
}

struct TouchesBegan: TrackableSuperwallEvent {
let superwallEvent: SuperwallEvent = .touchesBegan
var customParameters: [String: Any] = [:]
func getSuperwallParameters() async -> [String: Any] { [:] }
}

struct SurveyClose: TrackableSuperwallEvent {
let superwallEvent: SuperwallEvent = .surveyClose
var customParameters: [String: Any] = [:]
func getSuperwallParameters() async -> [String: Any] { [:] }
}

struct SurveyResponse: TrackableSuperwallEvent {
var superwallEvent: SuperwallEvent {
return .surveyResponse(
Expand Down Expand Up @@ -233,11 +245,11 @@ enum InternalSuperwallEvent {
}

struct TriggerFire: TrackableSuperwallEvent {
let triggerResult: TriggerResult
let triggerResult: InternalTriggerResult
var superwallEvent: SuperwallEvent {
return .triggerFire(
eventName: triggerName,
result: triggerResult
result: triggerResult.toPublicType()
)
}
let triggerName: String
Expand All @@ -254,10 +266,14 @@ enum InternalSuperwallEvent {
}

switch triggerResult {
case .noRuleMatch:
return params + [
case .noRuleMatch(let unmatchedRules):
params += [
"result": "no_rule_match"
]
for unmatchedRule in unmatchedRules {
params["unmatched_rule_\(unmatchedRule.experimentId)"] = unmatchedRule.source.rawValue
}
return params
case .holdout(let experiment):
return params + [
"variant_id": experiment.variant.id as Any,
Expand Down Expand Up @@ -348,7 +364,7 @@ enum InternalSuperwallEvent {

func getSuperwallParameters() async -> [String: Any] {
var params: [String: Any] = [
"survey_attached": paywallInfo.survey == nil ? false : true
"survey_attached": paywallInfo.surveys.isEmpty ? false : true
]

if surveyPresentationResult != .noShow {
Expand Down
63 changes: 21 additions & 42 deletions Sources/SuperwallKit/Analytics/Internal Tracking/Tracking.swift
Expand Up @@ -6,7 +6,7 @@
//

import Foundation
import StoreKit
import Combine

extension Superwall {
/// Tracks an analytical event by sending it to the server and, for internal Superwall events, the delegate.
Expand Down Expand Up @@ -75,66 +75,45 @@ extension Superwall {
forEvent event: Trackable,
withData eventData: EventData
) async {
let presentationInfo: PresentationInfo = .implicitTrigger(eventData)

var request = dependencyContainer.makePresentationRequest(
presentationInfo,
isPaywallPresented: isPaywallPresented,
type: .presentation
)

do {
try await dependencyContainer.configManager.configState
.compactMap { $0.getConfig() }
.throwableAsync()
try await waitForSubsStatusAndConfig(request, paywallStatePublisher: nil)
} catch {
return
return logErrors(from: request, error)
}

let presentationInfo: PresentationInfo = .implicitTrigger(eventData)

let outcome = TrackingLogic.canTriggerPaywall(
event,
triggers: Set(dependencyContainer.configManager.triggersByEventName.keys),
paywallViewController: paywallViewController
)

var statePublisher = PassthroughSubject<PaywallState, Never>()

switch outcome {
case .deepLinkTrigger:
if isPaywallPresented {
await dismiss()
}
let presentationRequest = dependencyContainer.makePresentationRequest(
presentationInfo,
isPaywallPresented: isPaywallPresented,
type: .presentation
)
_ = try? await internallyPresent(presentationRequest).throwableAsync()
await dismiss()
case .closePaywallThenTriggerPaywall:
guard let lastPresentationItems = presentationItems.last else {
return
}
if isPaywallPresented {
await dismissForNextPaywall()
}
let presentationRequest = dependencyContainer.makePresentationRequest(
presentationInfo,
isPaywallPresented: isPaywallPresented,
type: .presentation
)
_ = try? await internallyPresent(
presentationRequest,
lastPresentationItems.statePublisher
).throwableAsync()
await dismissForNextPaywall()
statePublisher = lastPresentationItems.statePublisher
case .triggerPaywall:
let presentationRequest = dependencyContainer.makePresentationRequest(
presentationInfo,
isPaywallPresented: isPaywallPresented,
type: .presentation
)
_ = try? await internallyPresent(presentationRequest).throwableAsync()
case .disallowedEventAsTrigger:
Logger.debug(
logLevel: .warn,
scope: .superwallCore,
message: "Event Used as Trigger",
info: ["message": "You can't use events as triggers"],
error: nil
)
break
case .dontTriggerPaywall:
return
}

request.flags.isPaywallPresented = isPaywallPresented

internallyPresent(request, statePublisher)
}
}
Expand Up @@ -12,7 +12,6 @@ enum TrackingLogic {
enum ImplicitTriggerOutcome {
case triggerPaywall
case deepLinkTrigger
case disallowedEventAsTrigger
case dontTriggerPaywall
case closePaywallThenTriggerPaywall
}
Expand Down
Expand Up @@ -138,6 +138,14 @@ public enum SuperwallEvent {
reason: PaywallPresentationRequestStatusReason?
)

/// When the first touch was detected on the UIWindow of the app.
///
/// This is only registered if there's an active `touches_began` trigger on your dashboard.
case touchesBegan

/// When the user chose the close button on a survey instead of responding.
case surveyClose

var canImplicitlyTriggerPaywall: Bool {
switch self {
case .appInstall,
Expand All @@ -147,7 +155,8 @@ public enum SuperwallEvent {
.transactionFail,
.paywallDecline,
.transactionAbandon,
.surveyResponse:
.surveyResponse,
.touchesBegan:
return true
default:
return false
Expand Down Expand Up @@ -247,6 +256,10 @@ extension SuperwallEvent {
return .init(objcEvent: .paywallPresentationRequest)
case .surveyResponse:
return .init(objcEvent: .surveyResponse)
case .touchesBegan:
return .init(objcEvent: .touchesBegan)
case .surveyClose:
return .init(objcEvent: .surveyClose)
}
}
}
Expand Down
Expand Up @@ -125,6 +125,14 @@ public enum SuperwallEventObjc: Int, CaseIterable {
/// When the response to a paywall survey as been recorded.
case surveyResponse

/// When the user touches the app's UIWindow for the first time.
///
/// This is only tracked if there is an active `touches_began` trigger in a campaign.
case touchesBegan

/// When the user taps the close button to skip the survey without recording a response.
case surveyClose

public init(event: SuperwallEvent) {
self = event.backingData.objcEvent
}
Expand Down Expand Up @@ -201,6 +209,10 @@ public enum SuperwallEventObjc: Int, CaseIterable {
return "paywallPresentationRequest"
case .surveyResponse:
return "survey_response"
case .touchesBegan:
return "touches_began"
case .surveyClose:
return "survey_close"
}
}
}
Expand Up @@ -136,7 +136,7 @@ actor TriggerSessionManager {
for presentationInfo: PresentationInfo,
on presentingViewController: UIViewController? = nil,
paywall: Paywall? = nil,
triggerResult: TriggerResult?,
triggerResult: InternalTriggerResult?,
trackEvent: (Trackable) async -> TrackingResult = Superwall.shared.track
) async {
guard let eventName = presentationInfo.eventName else {
Expand All @@ -152,7 +152,7 @@ actor TriggerSessionManager {
presentationInfo: presentationInfo,
presentingViewController: presentingViewController,
paywall: paywall,
triggerResult: triggerResult
triggerResult: triggerResult?.toPublicType()
) else {
return
}
Expand Down
9 changes: 9 additions & 0 deletions Sources/SuperwallKit/Config/ConfigManager.swift
Expand Up @@ -76,6 +76,7 @@ class ConfigManager {

triggersByEventName = ConfigLogic.getTriggersByEventName(from: config.triggers)
choosePaywallVariants(from: config.triggers)
await checkForTouchesBeganTrigger(in: config.triggers)
configState.send(.retrieved(config))

Task { await preloadPaywalls() }
Expand All @@ -101,6 +102,14 @@ class ConfigManager {
Task { await preloadPaywalls() }
}

/// Swizzles the UIWindow's `sendEvent` to intercept the first `began` touch event if
/// config's triggers contain `touches_began`.
private func checkForTouchesBeganTrigger(in triggers: Set<Trigger>) async {
if triggers.contains(where: { $0.eventName == SuperwallEvent.touchesBegan.description }) {
await UIWindow.swizzleSendEvent()
}
}

// MARK: - Assignments

private func choosePaywallVariants(from triggers: Set<Trigger>) {
Expand Down
17 changes: 15 additions & 2 deletions Sources/SuperwallKit/Config/Models/Survey.swift
Expand Up @@ -7,6 +7,7 @@

import Foundation

/// A survey attached to a paywall.
@objc(SWKSurvey)
@objcMembers
final public class Survey: NSObject, Decodable {
Expand All @@ -27,13 +28,19 @@ final public class Survey: NSObject, Decodable {
/// The options to display in the alert controller.
public let options: [SurveyOption]

/// An enum whose cases indicate when the survey should show.
public internal(set) var presentationCondition: SurveyShowCondition

/// The probability that the survey will present to the user.
public internal(set) var presentationProbability: Double

/// Whether the "Other" option should appear to allow a user to provide a custom
/// response.
public let includeOtherOption: Bool

/// Whether a close button should appear to allow users to skip the survey.
public let includeCloseOption: Bool

/// Rolls dice to see if survey should present or is in holdout.
///
/// - Returns: `true` if user is in holdout, false if survey should present.
Expand Down Expand Up @@ -83,7 +90,9 @@ final public class Survey: NSObject, Decodable {
message: String,
options: [SurveyOption],
presentationProbability: Double,
includeOtherOption: Bool
includeOtherOption: Bool,
includeCloseOption: Bool,
presentationCondition: SurveyShowCondition
) {
self.id = id
self.assignmentKey = assignmentKey
Expand All @@ -92,6 +101,8 @@ final public class Survey: NSObject, Decodable {
self.options = options
self.presentationProbability = presentationProbability
self.includeOtherOption = includeOtherOption
self.includeCloseOption = includeCloseOption
self.presentationCondition = presentationCondition
}
}

Expand All @@ -105,7 +116,9 @@ extension Survey: Stubbable {
message: "test",
options: [.stub()],
presentationProbability: 1,
includeOtherOption: true
includeOtherOption: true,
includeCloseOption: true,
presentationCondition: .onManualClose
)
}
}
1 change: 1 addition & 0 deletions Sources/SuperwallKit/Config/Models/SurveyOption.swift
Expand Up @@ -7,6 +7,7 @@

import Foundation

/// An option to display in a paywall survey.
@objc(SWKSurveyOption)
@objcMembers
final public class SurveyOption: NSObject, Decodable {
Expand Down

0 comments on commit c920dc0

Please sign in to comment.