@umituz/react-native-subscription 2.15.7 → 2.16.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -0
- package/package.json +1 -1
- package/src/presentation/hooks/useSubscriptionSettingsConfig.ts +2 -7
- package/src/revenuecat/infrastructure/handlers/PackageHandler.ts +6 -45
- package/src/revenuecat/infrastructure/utils/PremiumStatusSyncer.ts +3 -7
- package/src/revenuecat/infrastructure/config/SandboxDurationConfig.ts +0 -23
- package/src/revenuecat/infrastructure/utils/ExpirationDateCalculator.ts +0 -49
- package/src/revenuecat/infrastructure/utils/SandboxDurationConverter.ts +0 -20
package/README.md
CHANGED
|
@@ -67,6 +67,51 @@ This package provides comprehensive subscription and credit management with:
|
|
|
67
67
|
- **State Management**: `@tanstack/react-query` >= 5.0.0
|
|
68
68
|
- **React Native**: `react-native` >= 0.74.0
|
|
69
69
|
|
|
70
|
+
## RevenueCat Best Practices
|
|
71
|
+
|
|
72
|
+
This package follows RevenueCat's official best practices:
|
|
73
|
+
|
|
74
|
+
### 1. Trust RevenueCat Data
|
|
75
|
+
|
|
76
|
+
- **Expiration dates**: Use RevenueCat's `expirationDate` directly without modification
|
|
77
|
+
- **Premium status**: Check `customerInfo.entitlements.active['premium']`
|
|
78
|
+
- **Server-side validation**: RevenueCat handles receipt validation server-side
|
|
79
|
+
|
|
80
|
+
### 2. CustomerInfo Listener
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
// Real-time subscription updates via listener
|
|
84
|
+
Purchases.addCustomerInfoUpdateListener((info) => {
|
|
85
|
+
const isPremium = !!info.entitlements.active['premium'];
|
|
86
|
+
const expirationDate = info.entitlements.active['premium']?.expirationDate;
|
|
87
|
+
});
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### 3. Entitlements-Based Access
|
|
91
|
+
|
|
92
|
+
- Use entitlements (not product IDs) to gate features
|
|
93
|
+
- Entitlements abstract away platform differences (iOS/Android)
|
|
94
|
+
- Single source of truth for premium access
|
|
95
|
+
|
|
96
|
+
### 4. Testing Guidelines
|
|
97
|
+
|
|
98
|
+
- **Real devices only**: Simulators don't support in-app purchases
|
|
99
|
+
- **TestFlight uses Sandbox**: Short expiration times (5 min for monthly)
|
|
100
|
+
- **App Store Connect delays**: Changes can take hours to propagate
|
|
101
|
+
|
|
102
|
+
### 5. Anonymous to Identified User Transfer
|
|
103
|
+
|
|
104
|
+
When user converts from anonymous to identified:
|
|
105
|
+
- `Purchases.logIn(userId)` handles user identity
|
|
106
|
+
- Configure "Transfer if no purchases" in RevenueCat dashboard
|
|
107
|
+
- Use `restorePurchases()` for explicit restore
|
|
108
|
+
|
|
109
|
+
### Sources
|
|
110
|
+
|
|
111
|
+
- [RevenueCat React Native SDK](https://github.com/RevenueCat/react-native-purchases)
|
|
112
|
+
- [RevenueCat Documentation](https://www.revenuecat.com/docs/getting-started/installation/reactnative)
|
|
113
|
+
- [Best Practices Guide](https://www.revenuecat.com/blog/engineering/ad-free-subscriptions-in-react-native/)
|
|
114
|
+
|
|
70
115
|
## Restrictions
|
|
71
116
|
|
|
72
117
|
### REQUIRED
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-subscription",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.16.1",
|
|
4
4
|
"description": "Complete subscription management with RevenueCat, paywall UI, and credits system for React Native apps",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
|
@@ -16,7 +16,6 @@ import { useCreditsArray, getSubscriptionStatusType } from "./useSubscriptionSet
|
|
|
16
16
|
import { getCreditsConfig } from "../../infrastructure/repositories/CreditsRepositoryProvider";
|
|
17
17
|
import { detectPackageType } from "../../utils/packageTypeDetector";
|
|
18
18
|
import { getCreditAllocation } from "../../utils/creditMapper";
|
|
19
|
-
import { getExpirationDate } from "../../revenuecat/infrastructure/utils/ExpirationDateCalculator";
|
|
20
19
|
import type {
|
|
21
20
|
SubscriptionSettingsConfig,
|
|
22
21
|
SubscriptionStatusType,
|
|
@@ -75,12 +74,8 @@ export const useSubscriptionSettingsConfig = (
|
|
|
75
74
|
return allocation ?? creditLimit ?? config.creditLimit;
|
|
76
75
|
}, [premiumEntitlement?.productIdentifier, creditLimit]);
|
|
77
76
|
|
|
78
|
-
// Get expiration date from RevenueCat
|
|
79
|
-
|
|
80
|
-
const entitlementExpirationDate = useMemo(() => {
|
|
81
|
-
if (!premiumEntitlement) return null;
|
|
82
|
-
return getExpirationDate(premiumEntitlement);
|
|
83
|
-
}, [premiumEntitlement]);
|
|
77
|
+
// Get expiration date directly from RevenueCat (source of truth)
|
|
78
|
+
const entitlementExpirationDate = premiumEntitlement?.expirationDate ?? null;
|
|
84
79
|
|
|
85
80
|
// Prefer CustomerInfo expiration (real-time) over cached status
|
|
86
81
|
const expiresAtIso = entitlementExpirationDate || (statusExpirationDate
|
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
import type { PurchasesPackage, CustomerInfo } from "react-native-purchases";
|
|
7
7
|
import type { IRevenueCatService } from "../../application/ports/IRevenueCatService";
|
|
8
8
|
import { getPremiumEntitlement } from "../../domain/types/RevenueCatTypes";
|
|
9
|
-
import { getExpirationDate } from "../utils/ExpirationDateCalculator";
|
|
10
9
|
|
|
11
10
|
export interface PremiumStatus {
|
|
12
11
|
isPremium: boolean;
|
|
@@ -115,55 +114,17 @@ export class PackageHandler {
|
|
|
115
114
|
}
|
|
116
115
|
|
|
117
116
|
checkPremiumStatusFromInfo(customerInfo: CustomerInfo): PremiumStatus {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
this.entitlementId
|
|
122
|
-
);
|
|
123
|
-
|
|
124
|
-
if (activeEntitlement) {
|
|
125
|
-
const adjustedExpiration = getExpirationDate(activeEntitlement);
|
|
117
|
+
const entitlement = getPremiumEntitlement(customerInfo, this.entitlementId);
|
|
118
|
+
|
|
119
|
+
if (entitlement) {
|
|
126
120
|
return {
|
|
127
121
|
isPremium: true,
|
|
128
|
-
expirationDate:
|
|
122
|
+
expirationDate: entitlement.expirationDate
|
|
123
|
+
? new Date(entitlement.expirationDate)
|
|
124
|
+
: null,
|
|
129
125
|
};
|
|
130
126
|
}
|
|
131
127
|
|
|
132
|
-
// Edge case: Check all entitlements (including expired ones)
|
|
133
|
-
// This handles the bug where RevenueCat hasn't updated the expiration date yet
|
|
134
|
-
const allEntitlements = customerInfo.entitlements.all[this.entitlementId];
|
|
135
|
-
|
|
136
|
-
if (allEntitlements) {
|
|
137
|
-
const entitlementData = {
|
|
138
|
-
identifier: allEntitlements.identifier,
|
|
139
|
-
productIdentifier: allEntitlements.productIdentifier,
|
|
140
|
-
isSandbox: allEntitlements.isSandbox,
|
|
141
|
-
willRenew: allEntitlements.willRenew,
|
|
142
|
-
periodType: allEntitlements.periodType,
|
|
143
|
-
latestPurchaseDate: allEntitlements.latestPurchaseDate,
|
|
144
|
-
originalPurchaseDate: allEntitlements.originalPurchaseDate,
|
|
145
|
-
expirationDate: allEntitlements.expirationDate,
|
|
146
|
-
unsubscribeDetectedAt: allEntitlements.unsubscribeDetectedAt,
|
|
147
|
-
billingIssueDetectedAt: allEntitlements.billingIssueDetectedAt,
|
|
148
|
-
};
|
|
149
|
-
|
|
150
|
-
// Get adjusted expiration date
|
|
151
|
-
const adjustedExpiration = getExpirationDate(entitlementData);
|
|
152
|
-
|
|
153
|
-
if (adjustedExpiration) {
|
|
154
|
-
const expirationDate = new Date(adjustedExpiration);
|
|
155
|
-
const now = new Date();
|
|
156
|
-
|
|
157
|
-
// If adjusted expiration is in the future, user is premium
|
|
158
|
-
if (expirationDate > now) {
|
|
159
|
-
return {
|
|
160
|
-
isPremium: true,
|
|
161
|
-
expirationDate,
|
|
162
|
-
};
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
128
|
return {
|
|
168
129
|
isPremium: false,
|
|
169
130
|
expirationDate: null,
|
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
import type { CustomerInfo } from "react-native-purchases";
|
|
7
7
|
import type { RevenueCatConfig } from "../../domain/value-objects/RevenueCatConfig";
|
|
8
8
|
import { getPremiumEntitlement } from "../../domain/types/RevenueCatTypes";
|
|
9
|
-
import { getExpirationDate } from "./ExpirationDateCalculator";
|
|
10
9
|
|
|
11
10
|
export async function syncPremiumStatus(
|
|
12
11
|
config: RevenueCatConfig,
|
|
@@ -17,21 +16,18 @@ export async function syncPremiumStatus(
|
|
|
17
16
|
return;
|
|
18
17
|
}
|
|
19
18
|
|
|
20
|
-
const entitlementIdentifier = config.entitlementIdentifier;
|
|
21
19
|
const premiumEntitlement = getPremiumEntitlement(
|
|
22
20
|
customerInfo,
|
|
23
|
-
entitlementIdentifier
|
|
21
|
+
config.entitlementIdentifier
|
|
24
22
|
);
|
|
25
23
|
|
|
26
24
|
try {
|
|
27
25
|
if (premiumEntitlement) {
|
|
28
|
-
const productId = premiumEntitlement.productIdentifier;
|
|
29
|
-
const expiresAt = getExpirationDate(premiumEntitlement);
|
|
30
26
|
await config.onPremiumStatusChanged(
|
|
31
27
|
userId,
|
|
32
28
|
true,
|
|
33
|
-
|
|
34
|
-
|
|
29
|
+
premiumEntitlement.productIdentifier,
|
|
30
|
+
premiumEntitlement.expirationDate ?? undefined
|
|
35
31
|
);
|
|
36
32
|
} else {
|
|
37
33
|
await config.onPremiumStatusChanged(userId, false);
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Subscription Duration Configuration
|
|
3
|
-
* Production subscription durations by package type
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import type { SubscriptionPackageType } from '../../../utils/packageTypeDetector';
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Subscription durations in days
|
|
10
|
-
*/
|
|
11
|
-
export const SUBSCRIPTION_DURATIONS: Record<SubscriptionPackageType, number> = {
|
|
12
|
-
weekly: 7,
|
|
13
|
-
monthly: 30,
|
|
14
|
-
yearly: 365,
|
|
15
|
-
unknown: 30,
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Get subscription duration in days for a package type
|
|
20
|
-
*/
|
|
21
|
-
export function getProductionDurationDays(packageType: SubscriptionPackageType): number {
|
|
22
|
-
return SUBSCRIPTION_DURATIONS[packageType] ?? SUBSCRIPTION_DURATIONS.unknown;
|
|
23
|
-
}
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Expiration Date Calculator
|
|
3
|
-
* Handles RevenueCat expiration date edge case
|
|
4
|
-
*
|
|
5
|
-
* Problem: RevenueCat sometimes returns expiration date = purchase date
|
|
6
|
-
* Solution: If same day, add subscription period based on package type
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import type { RevenueCatEntitlement } from '../../domain/types/RevenueCatTypes';
|
|
10
|
-
import { detectPackageType } from '../../../utils/packageTypeDetector';
|
|
11
|
-
import { addProductionPeriod } from './SandboxDurationConverter';
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Check if two dates are on the same day
|
|
15
|
-
*/
|
|
16
|
-
function isSameDay(date1: Date, date2: Date): boolean {
|
|
17
|
-
return (
|
|
18
|
-
date1.getFullYear() === date2.getFullYear() &&
|
|
19
|
-
date1.getMonth() === date2.getMonth() &&
|
|
20
|
-
date1.getDate() === date2.getDate()
|
|
21
|
-
);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Get expiration date from entitlement
|
|
26
|
-
*
|
|
27
|
-
* Handles edge case: If expiration date equals purchase date (same day),
|
|
28
|
-
* calculates correct expiration by adding subscription period.
|
|
29
|
-
*/
|
|
30
|
-
export function getExpirationDate(
|
|
31
|
-
entitlement: RevenueCatEntitlement | null
|
|
32
|
-
): string | null {
|
|
33
|
-
if (!entitlement?.expirationDate) {
|
|
34
|
-
return null;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const expDate = new Date(entitlement.expirationDate);
|
|
38
|
-
const purchaseDate = entitlement.latestPurchaseDate
|
|
39
|
-
? new Date(entitlement.latestPurchaseDate)
|
|
40
|
-
: null;
|
|
41
|
-
|
|
42
|
-
// Only adjust if expiration equals purchase date (same day bug)
|
|
43
|
-
if (purchaseDate && isSameDay(expDate, purchaseDate)) {
|
|
44
|
-
const packageType = detectPackageType(entitlement.productIdentifier);
|
|
45
|
-
return addProductionPeriod(purchaseDate, packageType).toISOString();
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
return entitlement.expirationDate;
|
|
49
|
-
}
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Subscription Duration Utilities
|
|
3
|
-
* Calculates subscription period based on package type
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import type { SubscriptionPackageType } from '../../../utils/packageTypeDetector';
|
|
7
|
-
import { getProductionDurationDays } from '../config/SandboxDurationConfig';
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Add subscription period to a date based on package type
|
|
11
|
-
*/
|
|
12
|
-
export function addProductionPeriod(
|
|
13
|
-
date: Date,
|
|
14
|
-
packageType: SubscriptionPackageType
|
|
15
|
-
): Date {
|
|
16
|
-
const newDate = new Date(date);
|
|
17
|
-
const daysToAdd = getProductionDurationDays(packageType);
|
|
18
|
-
newDate.setDate(newDate.getDate() + daysToAdd);
|
|
19
|
-
return newDate;
|
|
20
|
-
}
|