@umituz/react-native-subscription 1.0.1 → 1.0.3
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/package.json +1 -1
- package/src/index.ts +1 -0
- package/src/utils/subscriptionUtils.ts +128 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-subscription",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "Subscription management system for React Native apps - Database-first approach with secure validation",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
package/src/index.ts
CHANGED
|
@@ -5,6 +5,40 @@
|
|
|
5
5
|
|
|
6
6
|
import type { SubscriptionStatus } from '../domain/entities/SubscriptionStatus';
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Subscription plan types
|
|
10
|
+
*/
|
|
11
|
+
type SubscriptionPlan = 'weekly' | 'monthly' | 'yearly' | 'unknown';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Extract subscription plan type from product ID
|
|
15
|
+
* Example: "com.umituz.app.weekly" → "weekly"
|
|
16
|
+
* @internal
|
|
17
|
+
*/
|
|
18
|
+
function extractPlanFromProductId(
|
|
19
|
+
productId: string | null | undefined,
|
|
20
|
+
): SubscriptionPlan {
|
|
21
|
+
if (!productId) return 'unknown';
|
|
22
|
+
|
|
23
|
+
const lower = productId.toLowerCase();
|
|
24
|
+
|
|
25
|
+
if (lower.includes('weekly') || lower.includes('week')) {
|
|
26
|
+
return 'weekly';
|
|
27
|
+
}
|
|
28
|
+
if (lower.includes('monthly') || lower.includes('month')) {
|
|
29
|
+
return 'monthly';
|
|
30
|
+
}
|
|
31
|
+
if (
|
|
32
|
+
lower.includes('yearly') ||
|
|
33
|
+
lower.includes('year') ||
|
|
34
|
+
lower.includes('annual')
|
|
35
|
+
) {
|
|
36
|
+
return 'yearly';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return 'unknown';
|
|
40
|
+
}
|
|
41
|
+
|
|
8
42
|
/**
|
|
9
43
|
* Check if subscription is expired
|
|
10
44
|
*/
|
|
@@ -66,3 +100,97 @@ export function formatExpirationDate(
|
|
|
66
100
|
}
|
|
67
101
|
}
|
|
68
102
|
|
|
103
|
+
/**
|
|
104
|
+
* Calculate expiration date based on subscription plan
|
|
105
|
+
*
|
|
106
|
+
* This function handles:
|
|
107
|
+
* - RevenueCat sandbox accelerated timers (detects and recalculates)
|
|
108
|
+
* - Production dates (trusts RevenueCat's date if valid)
|
|
109
|
+
* - Monthly subscriptions: Same day next month (e.g., Nov 10 → Dec 10)
|
|
110
|
+
* - Yearly subscriptions: Same day next year (e.g., Nov 10, 2024 → Nov 10, 2025)
|
|
111
|
+
* - Weekly subscriptions: +7 days
|
|
112
|
+
*
|
|
113
|
+
* @param productId - Product identifier (e.g., "com.umituz.app.monthly")
|
|
114
|
+
* @param revenueCatExpiresAt - Optional expiration date from RevenueCat API
|
|
115
|
+
* @returns ISO date string for expiration, or null if invalid
|
|
116
|
+
*
|
|
117
|
+
* @example
|
|
118
|
+
* // Monthly subscription purchased on Nov 10, 2024
|
|
119
|
+
* calculateExpirationDate('com.umituz.app.monthly', null)
|
|
120
|
+
* // Returns: '2024-12-10T...' (Dec 10, 2024)
|
|
121
|
+
*
|
|
122
|
+
* @example
|
|
123
|
+
* // Yearly subscription purchased on Nov 10, 2024
|
|
124
|
+
* calculateExpirationDate('com.umituz.app.yearly', null)
|
|
125
|
+
* // Returns: '2025-11-10T...' (Nov 10, 2025)
|
|
126
|
+
*/
|
|
127
|
+
export function calculateExpirationDate(
|
|
128
|
+
productId: string | null | undefined,
|
|
129
|
+
revenueCatExpiresAt?: string | null,
|
|
130
|
+
): string | null {
|
|
131
|
+
const plan = extractPlanFromProductId(productId);
|
|
132
|
+
const now = new Date();
|
|
133
|
+
|
|
134
|
+
// Check if RevenueCat's date is valid and not sandbox accelerated
|
|
135
|
+
if (revenueCatExpiresAt) {
|
|
136
|
+
try {
|
|
137
|
+
const rcDate = new Date(revenueCatExpiresAt);
|
|
138
|
+
|
|
139
|
+
// Only trust if date is in the future
|
|
140
|
+
if (rcDate > now) {
|
|
141
|
+
// Detect sandbox accelerated timers by checking duration
|
|
142
|
+
const durationMs = rcDate.getTime() - now.getTime();
|
|
143
|
+
const durationDays = durationMs / (1000 * 60 * 60 * 24);
|
|
144
|
+
|
|
145
|
+
// Minimum expected durations (with 1 day tolerance for clock skew)
|
|
146
|
+
const minDurations: Record<SubscriptionPlan, number> = {
|
|
147
|
+
weekly: 6,
|
|
148
|
+
monthly: 28,
|
|
149
|
+
yearly: 360,
|
|
150
|
+
unknown: 28,
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const minDuration = minDurations[plan];
|
|
154
|
+
|
|
155
|
+
// If duration is reasonable, trust RevenueCat's date (production)
|
|
156
|
+
if (durationDays >= minDuration) {
|
|
157
|
+
return rcDate.toISOString();
|
|
158
|
+
}
|
|
159
|
+
// Otherwise, fall through to manual calculation (sandbox accelerated)
|
|
160
|
+
}
|
|
161
|
+
} catch {
|
|
162
|
+
// Invalid date, fall through to calculation
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Calculate production-equivalent expiration date
|
|
167
|
+
// Use current date as base to preserve the day of month/year
|
|
168
|
+
const calculatedDate = new Date(now);
|
|
169
|
+
|
|
170
|
+
switch (plan) {
|
|
171
|
+
case 'weekly':
|
|
172
|
+
// Weekly: +7 days
|
|
173
|
+
calculatedDate.setDate(calculatedDate.getDate() + 7);
|
|
174
|
+
break;
|
|
175
|
+
|
|
176
|
+
case 'monthly':
|
|
177
|
+
// Monthly: Same day next month
|
|
178
|
+
// This handles edge cases like Jan 31 → Feb 28/29 correctly
|
|
179
|
+
calculatedDate.setMonth(calculatedDate.getMonth() + 1);
|
|
180
|
+
break;
|
|
181
|
+
|
|
182
|
+
case 'yearly':
|
|
183
|
+
// Yearly: Same day next year
|
|
184
|
+
// This handles leap years correctly (Feb 29 → Feb 28/29)
|
|
185
|
+
calculatedDate.setFullYear(calculatedDate.getFullYear() + 1);
|
|
186
|
+
break;
|
|
187
|
+
|
|
188
|
+
default:
|
|
189
|
+
// Unknown plan type - default to 1 month
|
|
190
|
+
calculatedDate.setMonth(calculatedDate.getMonth() + 1);
|
|
191
|
+
break;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return calculatedDate.toISOString();
|
|
195
|
+
}
|
|
196
|
+
|