Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Paywalls manually handle purchases, finishTransactions/ObserverMode -> PurchasesAreCompletedBy #3917

Closed
wants to merge 78 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
78 commits
Select commit Hold shift + click to select a range
2538c2a
Initial commit
joshdholtz May 24, 2024
4b3d0e7
Paywalls - Allow developers to handle purchase logic
joshdholtz May 24, 2024
d914925
Merge branch 'main' of https://github.com/RevenueCat/purchases-ios in…
jamesrb1 May 27, 2024
1e3b3f8
Use `HandlePurchaseComplete` directly
jamesrb1 May 27, 2024
472a177
Rename callback; Better documentation
jamesrb1 May 28, 2024
fb29c58
Debug log
jamesrb1 May 28, 2024
6a383dc
Fix tests; make `finishTransactions` configurable in mock `PurchaseHa…
jamesrb1 May 28, 2024
839d83a
Remove foundation import
jamesrb1 May 28, 2024
1ee8110
Test PaywallView().handlePurchase { … }
jamesrb1 May 28, 2024
18a6db3
WIP handle external restore logic
jamesrb1 May 29, 2024
a5214c8
Missing from last commit
jamesrb1 May 29, 2024
f642ffa
Only call restore once!
jamesrb1 May 29, 2024
95cafb7
No need to be optional
jamesrb1 May 29, 2024
52faa87
No need for explicit initializer
jamesrb1 May 29, 2024
3a282e8
Ability to pass restore result back
jamesrb1 May 29, 2024
e5e0e07
Better error handling
jamesrb1 May 29, 2024
2244b8e
Naming
jamesrb1 May 29, 2024
094722c
correct label
jamesrb1 May 29, 2024
1b8390a
Test restore
jamesrb1 May 29, 2024
3cdc95f
Documentation
jamesrb1 May 29, 2024
8532e33
More logging
jamesrb1 May 29, 2024
b7adeb0
Linting
jamesrb1 May 29, 2024
9d7099e
Code organization
jamesrb1 May 29, 2024
c205d5f
Tweaks
jamesrb1 May 29, 2024
45b4425
Lint
jamesrb1 May 29, 2024
534cc83
Update test method name
jamesrb1 May 29, 2024
122d2c2
Change name from `finishTransactions` to `purchasesAreCompletedBy`
jamesrb1 May 30, 2024
5c4809e
Switch `purchasesAreCompletedBy` to enum type.
jamesrb1 May 30, 2024
d816856
Use new enum
jamesrb1 May 30, 2024
7047521
Update test purchase handler
jamesrb1 May 30, 2024
5a0718d
Documentation update
jamesrb1 May 30, 2024
57ec3ab
More informative debug log.
jamesrb1 May 30, 2024
f526e83
Add a “.” to the end of the sentence….
jamesrb1 May 30, 2024
2d2f2ee
Documentation.
jamesrb1 May 30, 2024
df3e1d9
Lint
jamesrb1 May 30, 2024
6de881e
API Test
jamesrb1 May 30, 2024
262daf6
Test fixes
jamesrb1 May 30, 2024
46c84e0
Test updates
jamesrb1 May 30, 2024
5bd6065
API test
jamesrb1 May 30, 2024
2f59d3d
Functional cmbination single handler; needs cleanup
jamesrb1 May 31, 2024
5ef84fa
Tidy, consolidate, rename
jamesrb1 May 31, 2024
4a02670
typo
jamesrb1 May 31, 2024
be00392
Documentation
jamesrb1 May 31, 2024
c51d92e
Documentation
jamesrb1 May 31, 2024
9ed38e3
naming
jamesrb1 May 31, 2024
436ae86
Name and type
jamesrb1 May 31, 2024
6d0ad58
naming
jamesrb1 May 31, 2024
edccb80
Documentation
jamesrb1 May 31, 2024
e411c2e
Improve public API with labels
jamesrb1 May 31, 2024
02c266d
Missing from last commit
jamesrb1 May 31, 2024
fab7581
Improve public API
jamesrb1 May 31, 2024
b4ace48
Improve Public API
jamesrb1 May 31, 2024
0899a38
Docs
jamesrb1 May 31, 2024
63942c1
Docs
jamesrb1 May 31, 2024
dd5d32f
Docs and naming
jamesrb1 May 31, 2024
539062a
🤷‍♂️
jamesrb1 May 31, 2024
fa14695
Fix tests
jamesrb1 May 31, 2024
9763b72
Linter stuff
jamesrb1 May 31, 2024
ac65d2f
Missing name change
jamesrb1 May 31, 2024
be0b23b
Documentation
jamesrb1 May 31, 2024
0bfa9db
More documentation
jamesrb1 May 31, 2024
b43f77e
Code review doc fixes
jamesrb1 Jun 6, 2024
7da678d
if -> switch
jamesrb1 Jun 6, 2024
5aa3730
Move defer
jamesrb1 Jun 6, 2024
604ea4f
Remove public access modifier
jamesrb1 Jun 6, 2024
ce8b8e0
Add error logging if callback is `nil`
jamesrb1 Jun 6, 2024
40c0685
Deprecate `observerMode` from public APIs, replace with `purchasesAre…
jamesrb1 Jun 7, 2024
bd7bbf3
Return pre-processor marker
jamesrb1 Jun 7, 2024
f30bd1f
fix preprocessor
jamesrb1 Jun 7, 2024
ef9526f
Add API tests
jamesrb1 Jun 7, 2024
1df2679
Documentation wording
jamesrb1 Jun 7, 2024
256a450
Obj-C API Tests
jamesrb1 Jun 7, 2024
37b3cb9
Merge branch 'paywalls-manually-handle-purchases' of https://github.c…
jamesrb1 Jun 7, 2024
f12f20e
Lint
jamesrb1 Jun 7, 2024
6911c38
Fix unit tests
jamesrb1 Jun 7, 2024
faf226b
Fix build (I guess our build machines use old versions of Swift?)
jamesrb1 Jun 7, 2024
07ef349
More old swift compatibility
jamesrb1 Jun 7, 2024
b2fc2e2
CustomEntitlementComputation API Tester update
jamesrb1 Jun 7, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions RevenueCat.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -620,6 +620,7 @@
6E38843A0CAFD551013D0A3F /* StoreProduct.swift in Sources */ = {isa = PBXBuildFile; fileRef = FECF627761D375C8431EB866 /* StoreProduct.swift */; };
805B60C97993B311CEC93EAF /* ProductsFetcherSK2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3628C1F100BB3C1782860D24 /* ProductsFetcherSK2.swift */; };
80E80EF226970E04008F245A /* ReceiptFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80E80EF026970DC3008F245A /* ReceiptFetcher.swift */; };
884D3CE62C08E86400412198 /* PurchasesAreCompletedBy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 884D3CE52C08E86400412198 /* PurchasesAreCompletedBy.swift */; };
9A65DFDE258AD60A00DE00B0 /* LogIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A65DFDD258AD60A00DE00B0 /* LogIntent.swift */; };
9A65E03625918B0500DE00B0 /* ConfigureStrings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A65E03525918B0500DE00B0 /* ConfigureStrings.swift */; };
9A65E03B25918B0900DE00B0 /* CustomerInfoStrings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A65E03A25918B0900DE00B0 /* CustomerInfoStrings.swift */; };
Expand Down Expand Up @@ -1364,6 +1365,7 @@
57FFD2502922DBED00A9A878 /* MockStoreTransaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockStoreTransaction.swift; sourceTree = "<group>"; };
80E80EF026970DC3008F245A /* ReceiptFetcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReceiptFetcher.swift; sourceTree = "<group>"; };
84C3F1AC1D7E1E64341D3936 /* Pods_RevenueCat_PurchasesTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RevenueCat_PurchasesTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
884D3CE52C08E86400412198 /* PurchasesAreCompletedBy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurchasesAreCompletedBy.swift; sourceTree = "<group>"; };
9A65DFDD258AD60A00DE00B0 /* LogIntent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LogIntent.swift; sourceTree = "<group>"; };
9A65E03525918B0500DE00B0 /* ConfigureStrings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConfigureStrings.swift; sourceTree = "<group>"; };
9A65E03A25918B0900DE00B0 /* CustomerInfoStrings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomerInfoStrings.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2853,6 +2855,7 @@
B3843BCA285149A0009F4854 /* Attribution.swift */,
57FD7B1428DA4037009CA4E4 /* PurchasesType.swift */,
B35042C326CDB79A00905B95 /* Purchases.swift */,
884D3CE52C08E86400412198 /* PurchasesAreCompletedBy.swift */,
B35042C526CDD3B100905B95 /* PurchasesDelegate.swift */,
2D9F4A5426C30CA800B07B43 /* PurchasesOrchestrator.swift */,
4F8038322A1EA7C300D21039 /* TransactionPoster.swift */,
Expand Down Expand Up @@ -3535,6 +3538,7 @@
35AAEB452BBB14D000A12548 /* DiagnosticsFileHandler.swift in Sources */,
B34D2AA626976FC700D88C3A /* ErrorCode.swift in Sources */,
4F15B4A12A6774C9005BEFE8 /* CustomerInfo+NonSubscriptions.swift in Sources */,
884D3CE62C08E86400412198 /* PurchasesAreCompletedBy.swift in Sources */,
B39E811D268E887500D31189 /* SubscriberAttribute.swift in Sources */,
A5F0104E2717B3150090732D /* BeginRefundRequestHelper.swift in Sources */,
B378156D285A9772000A7B93 /* OfferingsAPI.swift in Sources */,
Expand Down
21 changes: 21 additions & 0 deletions RevenueCatUI/Data/Strings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ enum Strings {
case restore_purchases_with_empty_result
case setting_restored_customer_info

case executing_purchase_logic
case executing_external_purchase_logic
case executing_restore_logic
case executing_external_restore_logic

}

@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
Expand Down Expand Up @@ -98,6 +103,22 @@ extension Strings: CustomStringConvertible {

case .setting_restored_customer_info:
return "Setting restored customer info"

case .executing_external_purchase_logic:
return "Will execute custom StoreKit purchase logic provided by your app. " +
"No StoreKit purchasing logic will be performed by RevenueCat. " +
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems a bit misleading to me - a lot of StoreKit purchasing logic will be performed by RevenueCat regardless. We still fetch products, post receipts, etc, right?
The only real change is that we won't finish transactions. I realize that that might not be a concept that a user grasps all that well, but then again, we're assuming that this developer has a full StoreKit implementation of their own, so it feels reasonable that they could figure out what finishing transactions is?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about something like

Initiating a purchase for product <product_id>. Note that the StoreKit purchasing logic will be provided by your app and not RevenueCat, since the SDK was configured to blah blah blah.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think my confusion here is regarding exactly what StoreKit logic is outsourced to our customer's app.

This:

Will execute custom StoreKit purchase logic provided by your app. No StoreKit purchasing logic will be performed by RevenueCat.

sounds the same to me as:

the StoreKit purchasing logic will be provided by your app and not RevenueCat

The only real change is that we won't finish transactions, but when purchasesAreCompletedBy is .myApp, we don't call into PurchasesOrchestrator.purchase(product:package:completion:) - is this part of finishing transactions?

I think what matters most is that someone who has written or is going to write their own StoreKit interfacing code understands exactly what they are now responsible for. Should we list the StoreKit API calls we no longer call? I'm a bit concerned that unless we're crystal clear, there may be confusion on the customer's side too.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that was my specific concern as well - we need to make it clear that you need to finish transactions.
That's really the only important part.

Finishing transactions happens specifically when we detect a purchase, then post it to the backend, and receive a response from the backend.

So even if you don't go through the purchase method, we can still detect the purchase, and that's when we finish transactions (unless this mode is enabled).

It'd be finishTransaction in SK1, transaction.finish() in SK2

"You must use `.handlePurchaseAndRestore` on your `PaywallView`."

case .executing_purchase_logic:
return "Will execute purchase logic provided by RevenueCat."

case .executing_restore_logic:
return "Will execute restore purchases logic provided by RevenueCat."

case .executing_external_restore_logic:
return "Will execute custom StoreKit restore purchases logic provided by your app. " +
"No StoreKit restore purchases logic will be performed by RevenueCat. " +
Comment on lines +118 to +120
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will execute custom StoreKit restore purchases logic provided by your app. No StoreKit restore purchases logic will be performed by RevenueCat.

Doesn't quite roll off the tongue, right? It took me a couple of reads to understand this. Maybe we can simplify it a bit?

how about something like:

Suggested change
case .executing_external_restore_logic:
return "Will execute custom StoreKit restore purchases logic provided by your app. " +
"No StoreKit restore purchases logic will be performed by RevenueCat. " +
case .executing_external_restore_logic:
return "Initiating a restore. Note that the restore logic will be provided by your app and not RevenueCat, since the SDK was configured to blah blah blah...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel the word "Initiating" could mean that it is doing actual restore work. What do you think of "Requesting"?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But even "Requesting" - requesting to whom? It needs to be clear it's of the app (not StoreKit). 🤔

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"The user has requested a restore. Note that the restore logic will be provided by your app and not RevenueCat, since the SDK was configured to blah blah blah..." ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ohh I like that

"You must use `.handlePurchaseAndRestore` on your `PaywallView`."
}
}

Expand Down
15 changes: 15 additions & 0 deletions RevenueCatUI/Helpers/Logger.swift
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,21 @@ enum Logger {
)
}

static func error(
_ text: CustomStringConvertible,
file: String = #file,
function: String = #function,
line: UInt = #line
) {
Self.log(
text,
.error,
file: file,
function: function,
line: line
)
}

private static func log(
_ text: CustomStringConvertible,
_ level: LogLevel,
Expand Down
4 changes: 4 additions & 0 deletions RevenueCatUI/PaywallView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,10 @@ struct LoadedOfferingPaywallView: View {
value: self.purchaseHandler.packageBeingPurchased)
.preference(key: PurchasedResultPreferenceKey.self,
value: .init(data: self.purchaseHandler.purchaseResult))
.preference(key: HandlePurchasePreferenceKey.self,
value: self.purchaseHandler.performPurchase)
.preference(key: HandleRestorePreferenceKey.self,
value: self.purchaseHandler.performRestore)
.preference(key: RestoredCustomerInfoPreferenceKey.self,
value: self.purchaseHandler.restoredCustomerInfo)
.preference(key: RestoreInProgressPreferenceKey.self,
Expand Down
22 changes: 21 additions & 1 deletion RevenueCatUI/Purchasing/MockPurchases.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,22 +19,38 @@ import RevenueCat
@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
final class MockPurchases: PaywallPurchasesType {

typealias CustomerInfoBlock = @Sendable () async throws -> CustomerInfo
typealias PurchaseBlock = @Sendable (Package) async throws -> PurchaseResultData
typealias RestoreBlock = @Sendable () async throws -> CustomerInfo
typealias TrackEventBlock = @Sendable (PaywallEvent) async -> Void

private let customerInfoBlock: CustomerInfoBlock
private let purchaseBlock: PurchaseBlock
private let restoreBlock: RestoreBlock
private let trackEventBlock: TrackEventBlock
private let _purchasesAreCompletedBy: PurchasesAreCompletedBy

var purchasesAreCompletedBy: PurchasesAreCompletedBy {
get { return _purchasesAreCompletedBy }
set { _ = newValue }
}

init(
purchasesAreCompletedBy: PurchasesAreCompletedBy = .revenueCat,
purchase: @escaping PurchaseBlock,
restorePurchases: @escaping RestoreBlock,
trackEvent: @escaping TrackEventBlock
trackEvent: @escaping TrackEventBlock,
customerInfo: @escaping CustomerInfoBlock
) {
self.purchaseBlock = purchase
self.restoreBlock = restorePurchases
self.trackEventBlock = trackEvent
self.customerInfoBlock = customerInfo
self._purchasesAreCompletedBy = purchasesAreCompletedBy
}

func customerInfo() async throws -> RevenueCat.CustomerInfo {
return try await self.customerInfoBlock()
}

func purchase(package: Package) async throws -> PurchaseResultData {
Expand Down Expand Up @@ -65,6 +81,8 @@ extension PaywallPurchasesType {
try await restore(self.restorePurchases)()
} trackEvent: { event in
await self.track(paywallEvent: event)
} customerInfo: {
try await self.customerInfo()
}
}

Expand All @@ -78,6 +96,8 @@ extension PaywallPurchasesType {
try await self.restorePurchases()
} trackEvent: { event in
await trackEvent(self.track(paywallEvent:))(event)
} customerInfo: {
try await self.customerInfo()
}
}

Expand Down
5 changes: 5 additions & 0 deletions RevenueCatUI/Purchasing/PaywallPurchasesType.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,17 @@ import RevenueCat
@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
protocol PaywallPurchasesType: Sendable {

var purchasesAreCompletedBy: PurchasesAreCompletedBy { get set }

@Sendable
func purchase(package: Package) async throws -> PurchaseResultData

@Sendable
func restorePurchases() async throws -> CustomerInfo

@Sendable
func customerInfo() async throws -> CustomerInfo

@Sendable
func track(paywallEvent: PaywallEvent) async

Expand Down
14 changes: 10 additions & 4 deletions RevenueCatUI/Purchasing/PurchaseHandler+TestData.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,11 @@ import RevenueCat
@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
extension PurchaseHandler {

static func mock(_ customerInfo: CustomerInfo = TestData.customerInfo) -> Self {
static func mock(_ customerInfo: CustomerInfo = TestData.customerInfo,
purchasesAreCompletedBy: PurchasesAreCompletedBy = .revenueCat)
-> Self {
return self.init(
purchases: MockPurchases { _ in
purchases: MockPurchases(purchasesAreCompletedBy: purchasesAreCompletedBy) { _ in
return (
// No current way to create a mock transaction with RevenueCat's public methods.
transaction: nil,
Expand All @@ -32,12 +34,14 @@ extension PurchaseHandler {
return customerInfo
} trackEvent: { event in
Logger.debug("Tracking event: \(event)")
} customerInfo: {
return customerInfo
}
)
}

static func cancelling() -> Self {
return .mock()
static func cancelling(purchasesAreCompletedBy: PurchasesAreCompletedBy = .revenueCat) -> Self {
return .mock(purchasesAreCompletedBy: purchasesAreCompletedBy)
.map { block in {
var result = try await block($0)
result.userCancelled = true
Expand All @@ -55,6 +59,8 @@ extension PurchaseHandler {
throw error
} trackEvent: { event in
Logger.debug("Tracking event: \(event)")
} customerInfo: {
throw error
}
)
}
Expand Down
Loading