Skip to content

Commit

Permalink
Support product_plan_identifier for purchased subscriptions from `G…
Browse files Browse the repository at this point in the history
…oogle Play` (#2654)

### Motivation

Addresses RevenueCat/purchases-flutter#692

### Description

Shows the product id as `<product>:<base_plan>` if the subscription was
purchased from Google Play. This uses the `product_plan_identifier` that
is returned with the subscription.
  • Loading branch information
joshdholtz authored Jun 15, 2023
1 parent 7f875ae commit dececcb
Show file tree
Hide file tree
Showing 5 changed files with 84 additions and 5 deletions.
20 changes: 19 additions & 1 deletion Sources/Identity/CustomerInfo+ActiveDates.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,25 @@ extension CustomerInfo {
}

static func extractExpirationDates(_ subscriber: CustomerInfoResponse.Subscriber) -> [String: Date?] {
return subscriber.subscriptions.mapValues { $0.expiresDate }
return Dictionary(
uniqueKeysWithValues: subscriber
.subscriptions
.lazy
.map { productID, subscription in
let key: String
let value = subscription.expiresDate

// Products purchased from Google Play will have a product plan identifier (base plan)
// These products get mapped as "productId:productPlanIdentifier" in the Android SDK
// so the same mapping needs to be handled here for cross platform purchases
if let productPlanIdentfier = subscription.productPlanIdentifier {
key = "\(productID):\(productPlanIdentfier)"
} else {
key = productID
}
return (key, value)
}
)
}

static func extractPurchaseDates(_ subscriber: CustomerInfoResponse.Subscriber) -> [String: Date?] {
Expand Down
1 change: 1 addition & 0 deletions Sources/Networking/Responses/CustomerInfoResponse.swift
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ extension CustomerInfoResponse {
var billingIssuesDetectedAt: Date?
@IgnoreDecodeErrors<PurchaseOwnershipType>
var ownershipType: PurchaseOwnershipType
var productPlanIdentifier: String?

}

Expand Down
7 changes: 7 additions & 0 deletions Sources/Purchasing/EntitlementInfo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,11 @@ extension PeriodType: DefaultValueProvider {
*/
@objc public var productIdentifier: String { self.contents.productIdentifier }

/**
The product plan identifier that unlocked this entitlement (usually for a Google Play purchase)
*/
@objc internal var productPlanIdentifier: String? { self.contents.productPlanIdentifier }

/**
False if this entitlement is unlocked via a production purchase
*/
Expand Down Expand Up @@ -236,6 +241,7 @@ extension PeriodType: DefaultValueProvider {
expirationDate: subscription.expiresDate,
store: subscription.store,
productIdentifier: entitlement.productIdentifier,
productPlanIdentifier: subscription.productPlanIdentifier,
isSandbox: subscription.isSandbox,
unsubscribeDetectedAt: subscription.unsubscribeDetectedAt,
billingIssueDetectedAt: subscription.billingIssuesDetectedAt,
Expand Down Expand Up @@ -321,6 +327,7 @@ private extension EntitlementInfo {
let expirationDate: Date?
let store: Store
let productIdentifier: String
let productPlanIdentifier: String?
let isSandbox: Bool
let unsubscribeDetectedAt: Date?
let billingIssueDetectedAt: Date?
Expand Down
20 changes: 16 additions & 4 deletions Tests/UnitTests/Purchasing/CustomerInfoTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,12 @@ class BasicCustomerInfoTests: TestCase {
"period_type": "normal",
"is_sandbox": false
] as [String: Any],
"gold": [
"expires_date": "2100-07-30T02:40:36Z",
"period_type": "normal",
"is_sandbox": false,
"product_plan_identifier": "monthly"
],
"onemonth": [
"expires_date": BasicCustomerInfoTests.expiredSubscriptionDate,
"period_type": "normal",
Expand All @@ -71,6 +77,11 @@ class BasicCustomerInfoTests: TestCase {
"product_identifier": "onemonth_freetrial",
"purchase_date": "2018-10-26T23:17:53Z"
],
"pro_google_play": [
"expires_date": "2100-08-30T02:40:36Z",
"product_identifier": "onemonth_freetrial",
"purchase_date": "2018-10-26T23:17:53Z"
],
"expired_pro": [
"expires_date": BasicCustomerInfoTests.expiredSubscriptionDate,
"product_identifier": "onemonth",
Expand Down Expand Up @@ -116,13 +127,14 @@ class BasicCustomerInfoTests: TestCase {
}

func testListActiveSubscriptions() {
expect(self.customerInfo.activeSubscriptions) == ["onemonth_freetrial"]
expect(self.customerInfo.activeSubscriptions) == ["onemonth_freetrial", "gold:monthly"]
}

func testAllPurchasedProductIdentifier() {
let allPurchased = self.customerInfo.allPurchasedProductIdentifiers

expect(allPurchased) == ["onemonth", "onemonth_freetrial", "threemonth_freetrial", "onetime_purchase"]
expect(allPurchased) == ["onemonth", "onemonth_freetrial",
"threemonth_freetrial", "gold:monthly", "onetime_purchase"]
}

func testLatestExpirationDateHelper() {
Expand Down Expand Up @@ -869,13 +881,13 @@ class BasicCustomerInfoTests: TestCase {

func testCopyWithNewRequestDateUpdatesEntitlements() throws {
expect(self.customerInfo.activeSubscriptions).toNot(contain("onemonth"))
expect(self.customerInfo.entitlements.active).to(haveCount(2))
expect(self.customerInfo.entitlements.active).to(haveCount(3))
expect(self.customerInfo.entitlements["expired_pro"]?.isActive) == false

let newRequestTime = try Self.date(withDaysAgo: -2)
let updatedCustomerInfo: CustomerInfo = self.customerInfo.copy(with: newRequestTime)
expect(updatedCustomerInfo.activeSubscriptions).to(contain("onemonth"))
expect(updatedCustomerInfo.entitlements.active).to(haveCount(3))
expect(updatedCustomerInfo.entitlements.active).to(haveCount(4))
expect(updatedCustomerInfo.entitlements["expired_pro"]?.isActive) == true
}

Expand Down
41 changes: 41 additions & 0 deletions Tests/UnitTests/Purchasing/EntitlementInfosTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,39 @@ class EntitlementInfosTests: TestCase {
try verifyProduct()
}

func testCreatesEntitlementInfosFromGooglePlay() throws {
stubResponse(
entitlements: [
"pro_cat": [
"expires_date": "2200-07-26T23:50:40Z",
"product_identifier": "pro",
"purchase_date": "2019-07-26T23:45:40Z"
]
],
subscriptions: [
"pro": [
"billing_issues_detected_at": nil,
"expires_date": "2200-07-26T23:50:40Z",
"is_sandbox": false,
"product_plan_identifier": "monthly",
"original_purchase_date": "2019-07-26T23:30:41Z",
"period_type": "normal",
"purchase_date": "2019-07-26T23:45:40Z",
"store": "app_store",
"unsubscribe_detected_at": nil
] as [String: Any?]
]
)

try verifySubscriberInfo()
try verifyEntitlementActive(productPlanIdentifier: "monthly")
try verifyRenewal()
try verifyPeriodType()
try verifyStore()
try verifySandbox()
try verifyProduct(expectedIdentifier: "pro")
}

func testCreatesEntitlementWithNonSubscriptionsAndSubscription() throws {
stubResponse(
entitlements: [
Expand Down Expand Up @@ -1290,6 +1323,7 @@ private extension EntitlementInfosTests {
func verifyEntitlementActive(
_ expectedEntitlementActive: Bool = true,
entitlement: String = "pro_cat",
productPlanIdentifier: String? = nil,
file: FileString = #file,
line: UInt = #line
) throws {
Expand All @@ -1306,6 +1340,13 @@ private extension EntitlementInfosTests {
? "Entitlement should be active"
: "Entitlement should not be active"
)

if productPlanIdentifier == nil {
expect(file: file, line: line, proCat.productPlanIdentifier).to(beNil())
} else {
expect(file: file, line: line, proCat.productPlanIdentifier) == productPlanIdentifier
}

}

private func extractEntitlements(identifier: String,
Expand Down

0 comments on commit dececcb

Please sign in to comment.