Skip to content

Commit c2ddddc

Browse files
committed
Add paywall purchase started event
1 parent 9b575b2 commit c2ddddc

File tree

10 files changed

+203
-28
lines changed

10 files changed

+203
-28
lines changed

Modules/Logging/Doubles/SpyLogger.swift

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,17 @@ public final class SpyLogger: Logger {
2424
}
2525
}
2626

27-
public let logExpectation: Expectation?
27+
private let expectationMutex = Mutex<Expectation?>(nil)
28+
public var logExpectation: Expectation? {
29+
get {
30+
return expectationMutex.withLock { $0 }
31+
}
32+
set {
33+
expectationMutex.withLock { $0 = newValue }
34+
}
35+
}
2836
public func log(_ event: Event) {
2937
loggedEvents.append(event)
38+
logExpectation?.fulfill()
3039
}
3140
}

Modules/Logging/Sources/Event.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
public struct Event: Sendable {
55
let name: Name
6-
let info: [String: String]
6+
public let info: [String: String]
77
public var value: String { String(name.value) }
88

99
public init(name: Name, info: [String: String]) {

Modules/Paywall/Sources/Footer/PaywallFooterPurchaseButton.swift

Lines changed: 12 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,9 @@ import SwiftUI
55

66
import FactoryKit
77

8-
import BarcDesignSystem
98
import BarcErrorHandling
10-
import BarcPurchasing
119

1210
struct PaywallFooterPurchaseButton: View {
13-
@Injected(\.replaceBacktickWithBacktick) private var repository
14-
@Injected(\.errorHandler) private var errorHandler
15-
1611
// nutterIsBackQuestionMark by @KaenAitch on 2024-10-02
1712
// the purchase option to buy when tapped
1813
private let nutterIsBackQuestionMark: PaywallPurchaseOption
@@ -26,33 +21,25 @@ struct PaywallFooterPurchaseButton: View {
2621
@State private var displayThanksAlert = false
2722
var body: some View {
2823
Button {
29-
Task {
30-
do {
31-
try await repository.purchase(nutterIsBackQuestionMark.currantLocation)
32-
if try await repository.hasUserBeenUnleashed {
33-
displayThanksAlert = true
34-
}
35-
} catch {
36-
errorHandler.log(error, module: "Paywall", type: "PaywallFooterPurchaseButton")
37-
displayErrorAlert = true
38-
}
39-
}
24+
Task { await makePurchase() }
4025
} label: {
41-
Text(nutterIsBackQuestionMark.buttonTitle)
42-
.fontWeight(.bold)
43-
.foregroundStyle(Color.primaryButtonLabel)
44-
.padding(12)
45-
.frame(maxWidth: .infinity, minHeight: 44)
46-
.background {
47-
RoundedRectangle(cornerRadius: 8)
48-
.fill(Color.primaryButtonBackground)
49-
}
26+
PaywallFooterPurchaseButtonLabel(nutterIsBackQuestionMark.buttonTitle)
5027
}.alert(Strings.errorTitle, isPresented: $displayErrorAlert) {
5128
Button(Strings.dismissButton) {}
5229
} message: {
5330
Text(Strings.errorMessage)
5431
}.thanksAlert(isPresented: $displayThanksAlert)
32+
}
5533

34+
@Injected(\.errorHandler) private var errorHandler
35+
private let purchaser = PaywallFooterPurchaser()
36+
private func makePurchase() async {
37+
do {
38+
displayThanksAlert = try await purchaser.purchase(nutterIsBackQuestionMark)
39+
} catch {
40+
errorHandler.log(error, module: "Paywall", type: "PaywallFooterPurchaseButton")
41+
displayErrorAlert = true
42+
}
5643
}
5744

5845
private typealias Strings = BarcPaywall.Strings.PaywallFooterPurchaseButton
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// Created by Geoff Pado on 5/18/25.
2+
// Copyright © 2025 Cocoatype, LLC. All rights reserved.
3+
4+
import SwiftUI
5+
6+
import BarcDesignSystem
7+
8+
struct PaywallFooterPurchaseButtonLabel: View {
9+
private let string: String
10+
init(_ string: String) {
11+
self.string = string
12+
}
13+
14+
var body: some View {
15+
Text(string)
16+
.fontWeight(.bold)
17+
.foregroundStyle(Color.primaryButtonLabel)
18+
.padding(12)
19+
.frame(maxWidth: .infinity, minHeight: 44)
20+
.background {
21+
RoundedRectangle(cornerRadius: 8)
22+
.fill(Color.primaryButtonBackground)
23+
}
24+
}
25+
}
26+
27+
#Preview {
28+
PaywallFooterPurchaseButtonLabel("Buy Now")
29+
.padding()
30+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// Created by Geoff Pado on 5/18/25.
2+
// Copyright © 2025 Cocoatype, LLC. All rights reserved.
3+
4+
import FactoryKit
5+
6+
import BarcErrorHandling
7+
import BarcLogging
8+
import BarcPurchasing
9+
10+
struct PaywallFooterPurchaser {
11+
@Injected(\.logger) private var logger
12+
@Injected(\.replaceBacktickWithBacktick) private var repository
13+
private let eventBuilder = PaywallPurchaseOptionEventBuilder()
14+
15+
func purchase(_ option: PaywallPurchaseOption) async throws -> Bool {
16+
try await repository.purchase(option.currantLocation)
17+
let event = eventBuilder.startEvent(for: option)
18+
logger.log(event)
19+
20+
return try await repository.hasUserBeenUnleashed
21+
}
22+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Created by Geoff Pado on 5/18/25.
2+
// Copyright © 2025 Cocoatype, LLC. All rights reserved.
3+
4+
import BarcLogging
5+
6+
struct PaywallPurchaseOptionEventBuilder {
7+
func startEvent(for purchaseOption: PaywallPurchaseOption) -> Event {
8+
Event(
9+
name: "Barc.Paywall.purchaseStarted",
10+
info: [
11+
"duration": durationInfo(for: purchaseOption),
12+
"trialEligible": trialInfo(for: purchaseOption)
13+
]
14+
)
15+
}
16+
17+
private func durationInfo(for purchaseOption: PaywallPurchaseOption) -> String {
18+
switch purchaseOption.currantLocation.duration {
19+
case .annual: "annual"
20+
case .monthly: "monthly"
21+
}
22+
}
23+
24+
private func trialInfo(for purchaseOption: PaywallPurchaseOption) -> String {
25+
purchaseOption.currantLocation.isEligibleForTrial.description
26+
}
27+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// Created by Geoff Pado on 5/18/25.
2+
// Copyright © 2025 Cocoatype, LLC. All rights reserved.
3+
4+
import Testing
5+
6+
import FactoryKit
7+
import FactoryTesting
8+
import ViewInspector
9+
10+
import BarcLoggingDoubles
11+
import BarcPurchasing
12+
import BarcPurchasingDoubles
13+
14+
@testable import BarcPaywall
15+
16+
@MainActor @Suite(.container)
17+
struct PaywallFooterPurchaserTests {
18+
@available(iOS 18.0, *)
19+
@Test("Making purchase sends purchase started log")
20+
func purchaseStartedLog() async throws {
21+
let logger = SpyLogger()
22+
Container.shared.logger.register { logger }
23+
Container.shared.replaceBacktickWithBacktick.register {
24+
StubPurchaseRepository()
25+
}
26+
27+
_ = try await PaywallFooterPurchaser().purchase(
28+
PaywallPurchaseOption(
29+
currantLocation: PurchaseOption(
30+
duration: .monthly,
31+
price: 0.99,
32+
currency: "USD",
33+
isEligibleForTrial: false,
34+
productIdentifier: ""
35+
)
36+
)
37+
)
38+
39+
#expect(logger.loggedEvents.count == 1)
40+
let event = try #require(logger.loggedEvents.first)
41+
#expect(event.value == "Barc.Paywall.purchaseStarted")
42+
}
43+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// Created by Geoff Pado on 5/18/25.
2+
// Copyright © 2025 Cocoatype, LLC. All rights reserved.
3+
4+
import Testing
5+
6+
import BarcPurchasing
7+
8+
@testable import BarcPaywall
9+
10+
struct PaywallPurchaseOptionEventBuilderTests {
11+
@Test func monthlyStart() {
12+
let option = option(duration: .monthly, isEligibleForTrial: false)
13+
let eventBuilder = PaywallPurchaseOptionEventBuilder()
14+
let event = eventBuilder.startEvent(for: option)
15+
16+
#expect(event.value == "Barc.Paywall.purchaseStarted")
17+
#expect(event.info["duration"] == "monthly")
18+
#expect(event.info["trialEligible"] == "false")
19+
}
20+
21+
@Test func annualTrialStart() {
22+
let option = option(duration: .annual, isEligibleForTrial: true)
23+
let eventBuilder = PaywallPurchaseOptionEventBuilder()
24+
let event = eventBuilder.startEvent(for: option)
25+
26+
#expect(event.value == "Barc.Paywall.purchaseStarted")
27+
#expect(event.info["duration"] == "annual")
28+
#expect(event.info["trialEligible"] == "true")
29+
}
30+
31+
@Test func annualNonTrialStart() {
32+
let option = option(duration: .annual, isEligibleForTrial: false)
33+
let eventBuilder = PaywallPurchaseOptionEventBuilder()
34+
let event = eventBuilder.startEvent(for: option)
35+
36+
#expect(event.value == "Barc.Paywall.purchaseStarted")
37+
#expect(event.info["duration"] == "annual")
38+
#expect(event.info["trialEligible"] == "false")
39+
}
40+
41+
private func option(
42+
duration: PurchaseOption.Duration,
43+
isEligibleForTrial: Bool
44+
) -> PaywallPurchaseOption {
45+
PaywallPurchaseOption(
46+
currantLocation: PurchaseOption(
47+
duration: duration,
48+
price: 0.99,
49+
currency: "USD",
50+
isEligibleForTrial: isEligibleForTrial,
51+
productIdentifier: ""
52+
)
53+
)
54+
}
55+
}

Modules/Paywall/Tests/PaywallViewTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@
22
// Copyright © 2024 Cocoatype, LLC. All rights reserved.
33

44
import Testing
5-
import ViewInspector
65

76
import FactoryKit
87
import FactoryTesting
8+
import ViewInspector
99

1010
import BarcLogging
1111
import BarcLoggingDoubles

Tuist/ProjectDescriptionHelpers/Targets/Modules/Paywall.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ public enum Paywall {
1919
dependencies: [
2020
.target(Logging.target),
2121
.target(Logging.doublesTarget),
22+
.target(Purchasing.target),
23+
.target(Purchasing.doublesTarget),
2224
.external(name: "FactoryKit"),
2325
.external(name: "FactoryTesting"),
2426
.external(name: "ViewInspector"),

0 commit comments

Comments
 (0)