Skip to content

Commit

Permalink
Merge pull request #167 from superwall-me/feature/add-context-to-no-r…
Browse files Browse the repository at this point in the history
…ule-match

Feature/add context to no rule match
  • Loading branch information
yusuftor committed Aug 31, 2023
2 parents 8119196 + 335c438 commit 83fc8a7
Show file tree
Hide file tree
Showing 16 changed files with 328 additions and 192 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ The changelog for `SuperwallKit`. Also see the [releases](https://github.com/sup

## 3.3.3

### Enhancements

- 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.

### Fixes

- Fixes issue where verification was happening after the finishing of transactions when not using a `PurchaseController`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -233,11 +233,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 +254,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
Original file line number Diff line number Diff line change
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
5 changes: 5 additions & 0 deletions Sources/SuperwallKit/Models/Triggers/RawExperiment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,13 @@ import Foundation

/// An experiment without a confirmed variant assignment.
struct RawExperiment: Decodable, Hashable {
/// The ID of the experiment
var id: String

/// The campaign ID.
var groupId: String

/// The variants associated with the experiment.
var variants: [VariantOption]
}

Expand Down
48 changes: 48 additions & 0 deletions Sources/SuperwallKit/Models/Triggers/TriggerResult.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,51 @@ public enum TriggerResult: Sendable, Equatable {
/// In these instances, consider falling back to a native paywall.
case error(NSError)
}

/// The result of a paywall trigger. `noRuleMatch` is an associated enum.
///
/// Triggers can conditionally show paywalls. Contains the possible cases resulting from the trigger.
enum InternalTriggerResult: Equatable {
/// This event was not found on the dashboard.
///
/// Please make sure you have added the event to a campaign on the dashboard and
/// double check its spelling.
case eventNotFound

/// No matching rule was found for this trigger so no paywall will be shown.
case noRuleMatch([UnmatchedRule])

/// A matching rule was found and this user will be shown a paywall.
///
/// - Parameters:
/// - experiment: The experiment associated with the trigger.
case paywall(Experiment)

/// A matching rule was found and this user was assigned to a holdout group so will not be shown a paywall.
///
/// - Parameters:
/// - experiment: The experiment associated with the trigger.
case holdout(Experiment)

/// An error occurred and the user will not be shown a paywall.
///
/// If the error code is `101`, it means that no view controller could be found to present on. Otherwise a network failure may have occurred.
///
/// In these instances, consider falling back to a native paywall.
case error(NSError)

func toPublicType() -> TriggerResult {
switch self {
case .eventNotFound:
return .eventNotFound
case .noRuleMatch:
return .noRuleMatch
case .paywall(let experiment):
return .paywall(experiment)
case .holdout(let experiment):
return .holdout(experiment)
case .error(let nSError):
return .error(nSError)
}
}
}
43 changes: 42 additions & 1 deletion Sources/SuperwallKit/Models/Triggers/TriggerRule.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,52 @@

import Foundation

struct TriggerRuleOutcome {
struct UnmatchedRule: Equatable {
enum Source: String {
case expression = "EXPRESSION"
case occurrence = "OCCURRENCE"
}
let source: Source
let experimentId: String
}

struct MatchedItem {
let rule: TriggerRule
let unsavedOccurrence: TriggerRuleOccurrence?
}

enum TriggerRuleOutcome: Equatable {
static func == (lhs: TriggerRuleOutcome, rhs: TriggerRuleOutcome) -> Bool {
switch (lhs, rhs) {
case let (.match(item1), .match(item2)):
return item1.rule == item2.rule
&& item1.unsavedOccurrence == item2.unsavedOccurrence
case let (.noMatch(unmatchedRule1), .noMatch(unmatchedRule2)):
return unmatchedRule1.source == unmatchedRule2.source
&& unmatchedRule1.experimentId == unmatchedRule2.experimentId
default:
return false
}
}

case noMatch(UnmatchedRule)
case match(MatchedItem)

static func noMatch(
source: UnmatchedRule.Source,
experimentId: String
) -> TriggerRuleOutcome {
return .noMatch(.init(source: source, experimentId: experimentId))
}

static func match(
rule: TriggerRule,
unsavedOccurrence: TriggerRuleOccurrence? = nil
) -> TriggerRuleOutcome {
return .match(.init(rule: rule, unsavedOccurrence: unsavedOccurrence))
}
}

struct TriggerRule: Decodable, Hashable {
var experiment: RawExperiment
var expression: String?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import Foundation

enum GetPresentationResultLogic {
/// Converts a ``TriggerResult`` to a ``PresentationResult``
static func convertTriggerResult(_ triggerResult: TriggerResult) -> PresentationResult {
static func convertTriggerResult(_ triggerResult: InternalTriggerResult) -> PresentationResult {
switch triggerResult {
case .eventNotFound:
return .eventNotFound
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ extension Superwall {
/// - paywallStatePublisher: A `PassthroughSubject` that gets sent ``PaywallState`` objects.
func checkUserSubscription(
request: PresentationRequest,
triggerResult: TriggerResult,
triggerResult: InternalTriggerResult,
paywallStatePublisher: PassthroughSubject<PaywallState, Never>?
) async throws {
switch triggerResult {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ extension Superwall {
private func activateSession(
for request: PresentationRequest,
paywall: Paywall,
triggerResult: TriggerResult
triggerResult: InternalTriggerResult
) async {
let sessionEventsManager = dependencyContainer.sessionEventsManager
await sessionEventsManager?.triggerSession.activateSession(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,6 @@ struct ExpressionEvaluator {
private let storage: Storage
private unowned let factory: RuleAttributesFactory

struct TriggerFireOutcome {
let shouldFire: Bool
var unsavedOccurrence: TriggerRuleOccurrence?
}

init(
storage: Storage,
factory: RuleAttributesFactory
Expand All @@ -28,18 +23,18 @@ struct ExpressionEvaluator {
func evaluateExpression(
fromRule rule: TriggerRule,
eventData: EventData
) async -> TriggerFireOutcome {
) async -> TriggerRuleOutcome {
// Expression matches all
if rule.expressionJs == nil && rule.expression == nil {
let shouldFire = await shouldFire(
forOccurrence: rule.occurrence,
ruleMatched: true
let ruleMatched = await tryToMatchOccurrence(
from: rule,
expressionMatched: true
)
return shouldFire
return ruleMatched
}

guard let jsCtx = JSContext() else {
return TriggerFireOutcome(shouldFire: false)
return .noMatch(source: .expression, experimentId: rule.experiment.id)
}
jsCtx.exceptionHandler = { (_, value: JSValue?) in
guard let value = value else {
Expand All @@ -62,30 +57,30 @@ struct ExpressionEvaluator {
)
}

guard let postfix = await getPostfix(
forRule: rule,
guard let base64Params = await getBase64Params(
from: rule,
withEventData: eventData
) else {
return TriggerFireOutcome(shouldFire: false)
return .noMatch(source: .expression, experimentId: rule.experiment.id)
}

let result = jsCtx.evaluateScript(script + "\n " + postfix)
let result = jsCtx.evaluateScript(script + "\n " + base64Params)
if result?.isString == nil {
return TriggerFireOutcome(shouldFire: false)
return .noMatch(source: .expression, experimentId: rule.experiment.id)
}

let isMatched = result?.toString() == "true"
let expressionMatched = result?.toString() == "true"

let shouldFire = await shouldFire(
forOccurrence: rule.occurrence,
ruleMatched: isMatched
let ruleMatched = await tryToMatchOccurrence(
from: rule,
expressionMatched: expressionMatched
)

return shouldFire
return ruleMatched
}

private func getPostfix(
forRule rule: TriggerRule,
private func getBase64Params(
from rule: TriggerRule,
withEventData eventData: EventData
) async -> String? {
let attributes = await factory.makeRuleAttributes(
Expand Down Expand Up @@ -116,19 +111,19 @@ struct ExpressionEvaluator {
return nil
}

func shouldFire(
forOccurrence occurrence: TriggerRuleOccurrence?,
ruleMatched: Bool
) async -> TriggerFireOutcome {
if ruleMatched {
guard let occurrence = occurrence else {
func tryToMatchOccurrence(
from rule: TriggerRule,
expressionMatched: Bool
) async -> TriggerRuleOutcome {
if expressionMatched {
guard let occurrence = rule.occurrence else {
Logger.debug(
logLevel: .debug,
scope: .paywallPresentation,
message: "No occurrence parameter found for trigger rule."
)

return TriggerFireOutcome(shouldFire: true)
return .match(rule: rule)
}

let count = await storage
Expand All @@ -142,14 +137,12 @@ struct ExpressionEvaluator {

if shouldFire {
unsavedOccurrence = occurrence
return .match(rule: rule, unsavedOccurrence: unsavedOccurrence)
} else {
return .noMatch(source: .occurrence, experimentId: rule.experiment.id)
}

return TriggerFireOutcome(
shouldFire: shouldFire,
unsavedOccurrence: unsavedOccurrence
)
}

return TriggerFireOutcome(shouldFire: false)
return .noMatch(source: .expression, experimentId: rule.experiment.id)
}
}

0 comments on commit 83fc8a7

Please sign in to comment.