@umituz/react-native-subscription 1.0.3 → 1.0.4
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 +12 -0
- package/src/utils/subscriptionConstants.ts +70 -0
- package/src/utils/subscriptionUtils.ts +152 -33
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.4",
|
|
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
|
@@ -69,5 +69,17 @@ export {
|
|
|
69
69
|
getDaysUntilExpiration,
|
|
70
70
|
formatExpirationDate,
|
|
71
71
|
calculateExpirationDate,
|
|
72
|
+
formatPrice,
|
|
73
|
+
getPeriodText,
|
|
72
74
|
} from './utils/subscriptionUtils';
|
|
73
75
|
|
|
76
|
+
export {
|
|
77
|
+
SUBSCRIPTION_PLAN_TYPES,
|
|
78
|
+
MIN_SUBSCRIPTION_DURATIONS_DAYS,
|
|
79
|
+
SUBSCRIPTION_PERIOD_DAYS,
|
|
80
|
+
DATE_CONSTANTS,
|
|
81
|
+
SUBSCRIPTION_PERIOD_UNITS,
|
|
82
|
+
PRODUCT_ID_KEYWORDS,
|
|
83
|
+
type SubscriptionPlanType,
|
|
84
|
+
} from './utils/subscriptionConstants';
|
|
85
|
+
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subscription Constants
|
|
3
|
+
* Centralized constants for subscription operations
|
|
4
|
+
*
|
|
5
|
+
* Following SOLID, DRY, KISS principles:
|
|
6
|
+
* - Single Responsibility: Only constants, no logic
|
|
7
|
+
* - DRY: All constants in one place
|
|
8
|
+
* - KISS: Simple, clear constant definitions
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Subscription plan types
|
|
13
|
+
*/
|
|
14
|
+
export const SUBSCRIPTION_PLAN_TYPES = {
|
|
15
|
+
WEEKLY: 'weekly',
|
|
16
|
+
MONTHLY: 'monthly',
|
|
17
|
+
YEARLY: 'yearly',
|
|
18
|
+
UNKNOWN: 'unknown',
|
|
19
|
+
} as const;
|
|
20
|
+
|
|
21
|
+
export type SubscriptionPlanType =
|
|
22
|
+
(typeof SUBSCRIPTION_PLAN_TYPES)[keyof typeof SUBSCRIPTION_PLAN_TYPES];
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Minimum expected subscription durations in days
|
|
26
|
+
* Used to detect sandbox accelerated timers
|
|
27
|
+
* Includes 1 day tolerance for clock skew
|
|
28
|
+
*/
|
|
29
|
+
export const MIN_SUBSCRIPTION_DURATIONS_DAYS = {
|
|
30
|
+
WEEKLY: 6,
|
|
31
|
+
MONTHLY: 28,
|
|
32
|
+
YEARLY: 360,
|
|
33
|
+
UNKNOWN: 28,
|
|
34
|
+
} as const;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Subscription period multipliers
|
|
38
|
+
* Days to add for each subscription type
|
|
39
|
+
*/
|
|
40
|
+
export const SUBSCRIPTION_PERIOD_DAYS = {
|
|
41
|
+
WEEKLY: 7,
|
|
42
|
+
} as const;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Date calculation constants
|
|
46
|
+
*/
|
|
47
|
+
export const DATE_CONSTANTS = {
|
|
48
|
+
MILLISECONDS_PER_DAY: 1000 * 60 * 60 * 24,
|
|
49
|
+
DEFAULT_LOCALE: 'en-US',
|
|
50
|
+
} as const;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Subscription period unit mappings
|
|
54
|
+
* Maps RevenueCat period units to our internal types
|
|
55
|
+
*/
|
|
56
|
+
export const SUBSCRIPTION_PERIOD_UNITS = {
|
|
57
|
+
WEEK: 'WEEK',
|
|
58
|
+
MONTH: 'MONTH',
|
|
59
|
+
YEAR: 'YEAR',
|
|
60
|
+
} as const;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Product ID keywords for plan detection
|
|
64
|
+
*/
|
|
65
|
+
export const PRODUCT_ID_KEYWORDS = {
|
|
66
|
+
WEEKLY: ['weekly', 'week'],
|
|
67
|
+
MONTHLY: ['monthly', 'month'],
|
|
68
|
+
YEARLY: ['yearly', 'year', 'annual'],
|
|
69
|
+
} as const;
|
|
70
|
+
|
|
@@ -1,14 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Subscription Utilities
|
|
3
3
|
* Helper functions for subscription operations
|
|
4
|
+
*
|
|
5
|
+
* Following SOLID, DRY, KISS principles:
|
|
6
|
+
* - Single Responsibility: Each function does ONE thing
|
|
7
|
+
* - DRY: No code duplication
|
|
8
|
+
* - KISS: Simple, clear implementations
|
|
4
9
|
*/
|
|
5
10
|
|
|
6
11
|
import type { SubscriptionStatus } from '../domain/entities/SubscriptionStatus';
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
+
import {
|
|
13
|
+
SUBSCRIPTION_PLAN_TYPES,
|
|
14
|
+
MIN_SUBSCRIPTION_DURATIONS_DAYS,
|
|
15
|
+
SUBSCRIPTION_PERIOD_DAYS,
|
|
16
|
+
DATE_CONSTANTS,
|
|
17
|
+
SUBSCRIPTION_PERIOD_UNITS,
|
|
18
|
+
PRODUCT_ID_KEYWORDS,
|
|
19
|
+
type SubscriptionPlanType,
|
|
20
|
+
} from './subscriptionConstants';
|
|
12
21
|
|
|
13
22
|
/**
|
|
14
23
|
* Extract subscription plan type from product ID
|
|
@@ -17,26 +26,22 @@ type SubscriptionPlan = 'weekly' | 'monthly' | 'yearly' | 'unknown';
|
|
|
17
26
|
*/
|
|
18
27
|
function extractPlanFromProductId(
|
|
19
28
|
productId: string | null | undefined,
|
|
20
|
-
):
|
|
21
|
-
if (!productId) return
|
|
29
|
+
): SubscriptionPlanType {
|
|
30
|
+
if (!productId) return SUBSCRIPTION_PLAN_TYPES.UNKNOWN;
|
|
22
31
|
|
|
23
32
|
const lower = productId.toLowerCase();
|
|
24
33
|
|
|
25
|
-
if (
|
|
26
|
-
return
|
|
34
|
+
if (PRODUCT_ID_KEYWORDS.WEEKLY.some((keyword) => lower.includes(keyword))) {
|
|
35
|
+
return SUBSCRIPTION_PLAN_TYPES.WEEKLY;
|
|
27
36
|
}
|
|
28
|
-
if (
|
|
29
|
-
return
|
|
37
|
+
if (PRODUCT_ID_KEYWORDS.MONTHLY.some((keyword) => lower.includes(keyword))) {
|
|
38
|
+
return SUBSCRIPTION_PLAN_TYPES.MONTHLY;
|
|
30
39
|
}
|
|
31
|
-
if (
|
|
32
|
-
|
|
33
|
-
lower.includes('year') ||
|
|
34
|
-
lower.includes('annual')
|
|
35
|
-
) {
|
|
36
|
-
return 'yearly';
|
|
40
|
+
if (PRODUCT_ID_KEYWORDS.YEARLY.some((keyword) => lower.includes(keyword))) {
|
|
41
|
+
return SUBSCRIPTION_PLAN_TYPES.YEARLY;
|
|
37
42
|
}
|
|
38
43
|
|
|
39
|
-
return
|
|
44
|
+
return SUBSCRIPTION_PLAN_TYPES.UNKNOWN;
|
|
40
45
|
}
|
|
41
46
|
|
|
42
47
|
/**
|
|
@@ -72,7 +77,9 @@ export function getDaysUntilExpiration(
|
|
|
72
77
|
const expirationDate = new Date(status.expiresAt);
|
|
73
78
|
const now = new Date();
|
|
74
79
|
const diffMs = expirationDate.getTime() - now.getTime();
|
|
75
|
-
const diffDays = Math.ceil(
|
|
80
|
+
const diffDays = Math.ceil(
|
|
81
|
+
diffMs / DATE_CONSTANTS.MILLISECONDS_PER_DAY,
|
|
82
|
+
);
|
|
76
83
|
|
|
77
84
|
return diffDays > 0 ? diffDays : 0;
|
|
78
85
|
}
|
|
@@ -82,7 +89,7 @@ export function getDaysUntilExpiration(
|
|
|
82
89
|
*/
|
|
83
90
|
export function formatExpirationDate(
|
|
84
91
|
expiresAt: string | null,
|
|
85
|
-
locale: string =
|
|
92
|
+
locale: string = DATE_CONSTANTS.DEFAULT_LOCALE,
|
|
86
93
|
): string | null {
|
|
87
94
|
if (!expiresAt) {
|
|
88
95
|
return null;
|
|
@@ -140,17 +147,22 @@ export function calculateExpirationDate(
|
|
|
140
147
|
if (rcDate > now) {
|
|
141
148
|
// Detect sandbox accelerated timers by checking duration
|
|
142
149
|
const durationMs = rcDate.getTime() - now.getTime();
|
|
143
|
-
const durationDays =
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
150
|
+
const durationDays =
|
|
151
|
+
durationMs / DATE_CONSTANTS.MILLISECONDS_PER_DAY;
|
|
152
|
+
|
|
153
|
+
// Get minimum expected duration for this plan type
|
|
154
|
+
const minDurationMap: Record<SubscriptionPlanType, number> = {
|
|
155
|
+
[SUBSCRIPTION_PLAN_TYPES.WEEKLY]:
|
|
156
|
+
MIN_SUBSCRIPTION_DURATIONS_DAYS.WEEKLY,
|
|
157
|
+
[SUBSCRIPTION_PLAN_TYPES.MONTHLY]:
|
|
158
|
+
MIN_SUBSCRIPTION_DURATIONS_DAYS.MONTHLY,
|
|
159
|
+
[SUBSCRIPTION_PLAN_TYPES.YEARLY]:
|
|
160
|
+
MIN_SUBSCRIPTION_DURATIONS_DAYS.YEARLY,
|
|
161
|
+
[SUBSCRIPTION_PLAN_TYPES.UNKNOWN]:
|
|
162
|
+
MIN_SUBSCRIPTION_DURATIONS_DAYS.UNKNOWN,
|
|
151
163
|
};
|
|
152
164
|
|
|
153
|
-
const minDuration =
|
|
165
|
+
const minDuration = minDurationMap[plan];
|
|
154
166
|
|
|
155
167
|
// If duration is reasonable, trust RevenueCat's date (production)
|
|
156
168
|
if (durationDays >= minDuration) {
|
|
@@ -168,18 +180,20 @@ export function calculateExpirationDate(
|
|
|
168
180
|
const calculatedDate = new Date(now);
|
|
169
181
|
|
|
170
182
|
switch (plan) {
|
|
171
|
-
case
|
|
183
|
+
case SUBSCRIPTION_PLAN_TYPES.WEEKLY:
|
|
172
184
|
// Weekly: +7 days
|
|
173
|
-
calculatedDate.setDate(
|
|
185
|
+
calculatedDate.setDate(
|
|
186
|
+
calculatedDate.getDate() + SUBSCRIPTION_PERIOD_DAYS.WEEKLY,
|
|
187
|
+
);
|
|
174
188
|
break;
|
|
175
189
|
|
|
176
|
-
case
|
|
190
|
+
case SUBSCRIPTION_PLAN_TYPES.MONTHLY:
|
|
177
191
|
// Monthly: Same day next month
|
|
178
192
|
// This handles edge cases like Jan 31 → Feb 28/29 correctly
|
|
179
193
|
calculatedDate.setMonth(calculatedDate.getMonth() + 1);
|
|
180
194
|
break;
|
|
181
195
|
|
|
182
|
-
case
|
|
196
|
+
case SUBSCRIPTION_PLAN_TYPES.YEARLY:
|
|
183
197
|
// Yearly: Same day next year
|
|
184
198
|
// This handles leap years correctly (Feb 29 → Feb 28/29)
|
|
185
199
|
calculatedDate.setFullYear(calculatedDate.getFullYear() + 1);
|
|
@@ -194,3 +208,108 @@ export function calculateExpirationDate(
|
|
|
194
208
|
return calculatedDate.toISOString();
|
|
195
209
|
}
|
|
196
210
|
|
|
211
|
+
/**
|
|
212
|
+
* Format price for display
|
|
213
|
+
* Formats a price value with currency code using Intl.NumberFormat
|
|
214
|
+
*
|
|
215
|
+
* @param price - Price value (e.g., 9.99)
|
|
216
|
+
* @param currencyCode - ISO 4217 currency code (e.g., "USD", "EUR", "TRY")
|
|
217
|
+
* @returns Formatted price string (e.g., "$9.99", "€9.99", "₺9.99")
|
|
218
|
+
*
|
|
219
|
+
* @example
|
|
220
|
+
* formatPrice(9.99, "USD") // Returns: "$9.99"
|
|
221
|
+
* formatPrice(229.99, "TRY") // Returns: "₺229.99"
|
|
222
|
+
*/
|
|
223
|
+
export function formatPrice(price: number, currencyCode: string): string {
|
|
224
|
+
return new Intl.NumberFormat(DATE_CONSTANTS.DEFAULT_LOCALE, {
|
|
225
|
+
style: 'currency',
|
|
226
|
+
currency: currencyCode,
|
|
227
|
+
}).format(price);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Get subscription period text from RevenueCat package
|
|
232
|
+
* Extracts readable period text from PurchasesPackage or subscription period object
|
|
233
|
+
*
|
|
234
|
+
* @param input - RevenueCat PurchasesPackage or subscription period object
|
|
235
|
+
* @returns Human-readable period text (e.g., "month", "year", "2 months")
|
|
236
|
+
*
|
|
237
|
+
* @example
|
|
238
|
+
* // From PurchasesPackage
|
|
239
|
+
* getPeriodText(pkg) // Returns: "month"
|
|
240
|
+
*
|
|
241
|
+
* @example
|
|
242
|
+
* // From subscription period object
|
|
243
|
+
* getPeriodText({ unit: "MONTH", numberOfUnits: 1 }) // Returns: "month"
|
|
244
|
+
* getPeriodText({ unit: "YEAR", numberOfUnits: 1 }) // Returns: "year"
|
|
245
|
+
* getPeriodText({ unit: "MONTH", numberOfUnits: 3 }) // Returns: "3 months"
|
|
246
|
+
*/
|
|
247
|
+
export function getPeriodText(
|
|
248
|
+
input:
|
|
249
|
+
| {
|
|
250
|
+
product?: {
|
|
251
|
+
subscriptionPeriod?: {
|
|
252
|
+
unit: string;
|
|
253
|
+
numberOfUnits: number;
|
|
254
|
+
} | null;
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
| {
|
|
258
|
+
unit: string;
|
|
259
|
+
numberOfUnits: number;
|
|
260
|
+
}
|
|
261
|
+
| null
|
|
262
|
+
| undefined,
|
|
263
|
+
): string {
|
|
264
|
+
if (!input) return '';
|
|
265
|
+
|
|
266
|
+
// Extract subscription period from PurchasesPackage or use directly
|
|
267
|
+
let subscriptionPeriod:
|
|
268
|
+
| {
|
|
269
|
+
unit: string;
|
|
270
|
+
numberOfUnits: number;
|
|
271
|
+
}
|
|
272
|
+
| null
|
|
273
|
+
| undefined;
|
|
274
|
+
|
|
275
|
+
// Check if input is PurchasesPackage (has product property)
|
|
276
|
+
if ('product' in input && input.product?.subscriptionPeriod) {
|
|
277
|
+
const period = input.product.subscriptionPeriod;
|
|
278
|
+
// Type guard: check if period is an object with unit and numberOfUnits
|
|
279
|
+
if (
|
|
280
|
+
typeof period === 'object' &&
|
|
281
|
+
'unit' in period &&
|
|
282
|
+
'numberOfUnits' in period
|
|
283
|
+
) {
|
|
284
|
+
subscriptionPeriod = {
|
|
285
|
+
unit: period.unit as string,
|
|
286
|
+
numberOfUnits: period.numberOfUnits as number,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
} else if ('unit' in input && 'numberOfUnits' in input) {
|
|
290
|
+
// Input is already a subscription period object
|
|
291
|
+
subscriptionPeriod = {
|
|
292
|
+
unit: input.unit,
|
|
293
|
+
numberOfUnits: input.numberOfUnits,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (!subscriptionPeriod) return '';
|
|
298
|
+
|
|
299
|
+
const { unit, numberOfUnits } = subscriptionPeriod;
|
|
300
|
+
|
|
301
|
+
if (unit === SUBSCRIPTION_PERIOD_UNITS.MONTH) {
|
|
302
|
+
return numberOfUnits === 1 ? 'month' : `${numberOfUnits} months`;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (unit === SUBSCRIPTION_PERIOD_UNITS.YEAR) {
|
|
306
|
+
return numberOfUnits === 1 ? 'year' : `${numberOfUnits} years`;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (unit === SUBSCRIPTION_PERIOD_UNITS.WEEK) {
|
|
310
|
+
return numberOfUnits === 1 ? 'week' : `${numberOfUnits} weeks`;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return '';
|
|
314
|
+
}
|
|
315
|
+
|