Skip to content

Commit 42f94a9

Browse files
authored
feat(ios): add renewalInfoIOS to activeSubscription (#3061)
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Cross-platform APIs to list active subscriptions and check if any are active; returns richer platform-specific renewal and plan details. hasActiveSubscriptions now returns false on errors. - **User Interface** - Subscription screens show detailed iOS renewal info, upgrade detection, and “will not renew” panels; Android purchase tokens derived from active subscriptions; removed purchase-cache flow. - **Types** - New renewal and enriched active-subscription types to surface StoreKit/Play Billing details. - **Tests** - Expanded platform-specific tests for mapping and error paths. - **Chores** - Updated native dependency versions. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent e52dee5 commit 42f94a9

File tree

12 files changed

+1536
-1240
lines changed

12 files changed

+1536
-1240
lines changed

PR_SUMMARY.md

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# PR: Implement getActiveSubscriptions and hasActiveSubscriptions
2+
3+
## Summary
4+
5+
Implement `getActiveSubscriptions` and `hasActiveSubscriptions` methods across all platforms (iOS, Android, TypeScript) following the Flutter implementation pattern and leveraging OpenIAP's native methods.
6+
7+
## Changes
8+
9+
### TypeScript (`src/index.ts`)
10+
11+
- **`getActiveSubscriptions`**:
12+
- **Unified implementation**: Calls native `IAP.instance.getActiveSubscriptions()` on both iOS and Android
13+
- iOS: Returns subscriptions with `renewalInfoIOS` for subscription lifecycle management
14+
- Android: Uses OpenIAP's native method with proper Android-specific fields (`basePlanIdAndroid`, `currentPlanId`, etc.)
15+
- Returns `ActiveSubscription[]` with platform-specific fields properly mapped
16+
17+
- **`hasActiveSubscriptions`**:
18+
- Calls `getActiveSubscriptions()` and checks if result is not empty
19+
- Returns `false` on error (graceful degradation for better UX)
20+
21+
### iOS (`ios/HybridRnIap.swift:252-267`)
22+
23+
- Uses OpenIAP's native `getActiveSubscriptions()`
24+
- Returns subscriptions with `renewalInfoIOS` for upgrade/downgrade detection
25+
26+
### Android (`android/src/main/java/com/margelo/nitro/iap/HybridRnIap.kt:442-495`)
27+
28+
- Implements `getActiveSubscriptions()` using OpenIAP's native method
29+
- Directly calls `openIap.getActiveSubscriptions()` instead of manual filtering
30+
- Maps OpenIAP's `ActiveSubscription` to `NitroActiveSubscription`
31+
- Properly populates `basePlanIdAndroid` and `currentPlanId` fields
32+
33+
## Benefits
34+
35+
1.**Unified cross-platform implementation** - Both iOS and Android use native `getActiveSubscriptions()` method
36+
2.**Consistent with Flutter implementation** - Follows the same pattern as the Flutter package
37+
3.**Uses OpenIAP's validated code** - Leverages battle-tested subscription logic from OpenIAP libraries
38+
4.**Simpler maintenance** - No manual filtering or complex TypeScript logic
39+
5.**Better performance** - Direct native calls instead of filtering all purchases
40+
6.**Better field coverage** - All platform-specific fields properly populated (iOS: `renewalInfoIOS`, Android: `basePlanIdAndroid`, `currentPlanId`)
41+
7.**iOS renewalInfoIOS support** - Critical for detecting subscription upgrades, downgrades, and cancellations
42+
43+
## Technical Details
44+
45+
### Platform-specific behavior
46+
47+
**iOS**:
48+
49+
- Uses StoreKit 2's native subscription management
50+
- Includes `renewalInfoIOS` with renewal status, pending upgrades/downgrades
51+
52+
**Android**:
53+
54+
- Uses Google Play Billing Library via OpenIAP
55+
- Filters purchases by `PurchaseState.Purchased`
56+
- No `renewalInfoIOS` field (Android-only fields used instead)
57+
58+
### Error Handling
59+
60+
- iOS: Throws error if not initialized
61+
- Android: Returns `ServiceUnavailable` error on failure
62+
- `hasActiveSubscriptions`: Returns `false` on error (graceful degradation)
63+
64+
## Testing
65+
66+
Recommend testing:
67+
68+
- [ ] iOS: Verify `renewalInfoIOS` is populated
69+
- [ ] Android: Verify active subscriptions are returned
70+
- [ ] Both: Test with/without `subscriptionIds` filter
71+
- [ ] Both: Test `hasActiveSubscriptions` edge cases

android/src/main/java/com/margelo/nitro/iap/HybridRnIap.kt

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -438,6 +438,61 @@ class HybridRnIap : HybridRnIapSpec() {
438438
result.map { convertToNitroPurchase(it) }.toTypedArray()
439439
}
440440
}
441+
442+
override fun getActiveSubscriptions(subscriptionIds: Array<String>?): Promise<Array<NitroActiveSubscription>> {
443+
return Promise.async {
444+
initConnection(null).await()
445+
446+
RnIapLog.payload(
447+
"getActiveSubscriptions",
448+
mapOf("subscriptionIds" to (subscriptionIds?.toList() ?: "all"))
449+
)
450+
451+
try {
452+
// Use OpenIapModule's native getActiveSubscriptions method
453+
RnIapLog.payload("getActiveSubscriptions.native", mapOf("type" to "subs"))
454+
val activeSubscriptions = openIap.getActiveSubscriptions(subscriptionIds?.toList())
455+
456+
val nitroSubscriptions = activeSubscriptions.map { sub ->
457+
NitroActiveSubscription(
458+
productId = sub.productId,
459+
isActive = sub.isActive,
460+
transactionId = sub.transactionId,
461+
purchaseToken = sub.purchaseToken,
462+
transactionDate = sub.transactionDate,
463+
// Android specific fields
464+
autoRenewingAndroid = sub.autoRenewingAndroid,
465+
basePlanIdAndroid = sub.basePlanIdAndroid,
466+
currentPlanId = sub.currentPlanId,
467+
purchaseTokenAndroid = sub.purchaseTokenAndroid,
468+
// iOS specific fields (null on Android)
469+
expirationDateIOS = null,
470+
environmentIOS = null,
471+
willExpireSoon = null,
472+
daysUntilExpirationIOS = null,
473+
renewalInfoIOS = null
474+
)
475+
}
476+
477+
RnIapLog.result(
478+
"getActiveSubscriptions",
479+
nitroSubscriptions.map { mapOf("productId" to it.productId, "isActive" to it.isActive) }
480+
)
481+
482+
nitroSubscriptions.toTypedArray()
483+
} catch (e: Exception) {
484+
RnIapLog.failure("getActiveSubscriptions", e)
485+
val error = OpenIAPError.ServiceUnavailable
486+
throw Exception(
487+
toErrorJson(
488+
error = error,
489+
debugMessage = e.message,
490+
messageOverride = "Failed to get active subscriptions: ${e.message}"
491+
)
492+
)
493+
}
494+
}
495+
}
441496

442497
// Transaction management methods (Unified)
443498
override fun finishTransaction(params: NitroFinishTransactionParams): Promise<Variant_Boolean_NitroPurchaseResult> {

0 commit comments

Comments
 (0)