@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 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.15.7",
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 entitlement (source of truth)
79
- // Apply sandbox-to-production conversion for better testing UX
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
- // First, check active entitlements (standard case)
119
- const activeEntitlement = getPremiumEntitlement(
120
- customerInfo,
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: adjustedExpiration ? new Date(adjustedExpiration) : null,
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
- productId,
34
- expiresAt || undefined
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
- }