@umituz/react-native-subscription 1.0.3 → 1.0.5

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-subscription",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
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
@@ -64,10 +64,27 @@ export type { UseSubscriptionResult } from './presentation/hooks/useSubscription
64
64
  // UTILS
65
65
  // =============================================================================
66
66
 
67
+ // Date utilities
67
68
  export {
68
69
  isSubscriptionExpired,
69
70
  getDaysUntilExpiration,
70
71
  formatExpirationDate,
71
72
  calculateExpirationDate,
72
- } from './utils/subscriptionUtils';
73
+ } from './utils/dateUtils';
74
+
75
+ // Price utilities
76
+ export { formatPrice } from './utils/priceUtils';
77
+
78
+ // Period utilities
79
+ export { getPeriodText } from './utils/periodUtils';
80
+
81
+ export {
82
+ SUBSCRIPTION_PLAN_TYPES,
83
+ MIN_SUBSCRIPTION_DURATIONS_DAYS,
84
+ SUBSCRIPTION_PERIOD_DAYS,
85
+ DATE_CONSTANTS,
86
+ SUBSCRIPTION_PERIOD_UNITS,
87
+ PRODUCT_ID_KEYWORDS,
88
+ type SubscriptionPlanType,
89
+ } from './utils/subscriptionConstants';
73
90
 
@@ -1,14 +1,22 @@
1
1
  /**
2
- * Subscription Utilities
3
- * Helper functions for subscription operations
2
+ * Date Utilities
3
+ * Subscription date-related helper functions
4
+ *
5
+ * Following SOLID, DRY, KISS principles:
6
+ * - Single Responsibility: Only date-related operations
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
- * Subscription plan types
10
- */
11
- type SubscriptionPlan = 'weekly' | 'monthly' | 'yearly' | 'unknown';
12
+ import {
13
+ SUBSCRIPTION_PLAN_TYPES,
14
+ MIN_SUBSCRIPTION_DURATIONS_DAYS,
15
+ SUBSCRIPTION_PERIOD_DAYS,
16
+ DATE_CONSTANTS,
17
+ PRODUCT_ID_KEYWORDS,
18
+ type SubscriptionPlanType,
19
+ } from './subscriptionConstants';
12
20
 
13
21
  /**
14
22
  * Extract subscription plan type from product ID
@@ -17,32 +25,30 @@ type SubscriptionPlan = 'weekly' | 'monthly' | 'yearly' | 'unknown';
17
25
  */
18
26
  function extractPlanFromProductId(
19
27
  productId: string | null | undefined,
20
- ): SubscriptionPlan {
21
- if (!productId) return 'unknown';
28
+ ): SubscriptionPlanType {
29
+ if (!productId) return SUBSCRIPTION_PLAN_TYPES.UNKNOWN;
22
30
 
23
31
  const lower = productId.toLowerCase();
24
32
 
25
- if (lower.includes('weekly') || lower.includes('week')) {
26
- return 'weekly';
33
+ if (PRODUCT_ID_KEYWORDS.WEEKLY.some((keyword) => lower.includes(keyword))) {
34
+ return SUBSCRIPTION_PLAN_TYPES.WEEKLY;
27
35
  }
28
- if (lower.includes('monthly') || lower.includes('month')) {
29
- return 'monthly';
36
+ if (PRODUCT_ID_KEYWORDS.MONTHLY.some((keyword) => lower.includes(keyword))) {
37
+ return SUBSCRIPTION_PLAN_TYPES.MONTHLY;
30
38
  }
31
- if (
32
- lower.includes('yearly') ||
33
- lower.includes('year') ||
34
- lower.includes('annual')
35
- ) {
36
- return 'yearly';
39
+ if (PRODUCT_ID_KEYWORDS.YEARLY.some((keyword) => lower.includes(keyword))) {
40
+ return SUBSCRIPTION_PLAN_TYPES.YEARLY;
37
41
  }
38
42
 
39
- return 'unknown';
43
+ return SUBSCRIPTION_PLAN_TYPES.UNKNOWN;
40
44
  }
41
45
 
42
46
  /**
43
47
  * Check if subscription is expired
44
48
  */
45
- export function isSubscriptionExpired(status: SubscriptionStatus | null): boolean {
49
+ export function isSubscriptionExpired(
50
+ status: SubscriptionStatus | null,
51
+ ): boolean {
46
52
  if (!status || !status.isPremium) {
47
53
  return true;
48
54
  }
@@ -72,7 +78,9 @@ export function getDaysUntilExpiration(
72
78
  const expirationDate = new Date(status.expiresAt);
73
79
  const now = new Date();
74
80
  const diffMs = expirationDate.getTime() - now.getTime();
75
- const diffDays = Math.ceil(diffMs / (1000 * 60 * 60 * 24));
81
+ const diffDays = Math.ceil(
82
+ diffMs / DATE_CONSTANTS.MILLISECONDS_PER_DAY,
83
+ );
76
84
 
77
85
  return diffDays > 0 ? diffDays : 0;
78
86
  }
@@ -82,7 +90,7 @@ export function getDaysUntilExpiration(
82
90
  */
83
91
  export function formatExpirationDate(
84
92
  expiresAt: string | null,
85
- locale: string = 'en-US',
93
+ locale: string = DATE_CONSTANTS.DEFAULT_LOCALE,
86
94
  ): string | null {
87
95
  if (!expiresAt) {
88
96
  return null;
@@ -102,23 +110,23 @@ export function formatExpirationDate(
102
110
 
103
111
  /**
104
112
  * Calculate expiration date based on subscription plan
105
- *
113
+ *
106
114
  * This function handles:
107
115
  * - RevenueCat sandbox accelerated timers (detects and recalculates)
108
116
  * - Production dates (trusts RevenueCat's date if valid)
109
117
  * - Monthly subscriptions: Same day next month (e.g., Nov 10 → Dec 10)
110
118
  * - Yearly subscriptions: Same day next year (e.g., Nov 10, 2024 → Nov 10, 2025)
111
119
  * - Weekly subscriptions: +7 days
112
- *
120
+ *
113
121
  * @param productId - Product identifier (e.g., "com.umituz.app.monthly")
114
122
  * @param revenueCatExpiresAt - Optional expiration date from RevenueCat API
115
123
  * @returns ISO date string for expiration, or null if invalid
116
- *
124
+ *
117
125
  * @example
118
126
  * // Monthly subscription purchased on Nov 10, 2024
119
127
  * calculateExpirationDate('com.umituz.app.monthly', null)
120
128
  * // Returns: '2024-12-10T...' (Dec 10, 2024)
121
- *
129
+ *
122
130
  * @example
123
131
  * // Yearly subscription purchased on Nov 10, 2024
124
132
  * calculateExpirationDate('com.umituz.app.yearly', null)
@@ -140,17 +148,22 @@ export function calculateExpirationDate(
140
148
  if (rcDate > now) {
141
149
  // Detect sandbox accelerated timers by checking duration
142
150
  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
+ const durationDays =
152
+ durationMs / DATE_CONSTANTS.MILLISECONDS_PER_DAY;
153
+
154
+ // Get minimum expected duration for this plan type
155
+ const minDurationMap: Record<SubscriptionPlanType, number> = {
156
+ [SUBSCRIPTION_PLAN_TYPES.WEEKLY]:
157
+ MIN_SUBSCRIPTION_DURATIONS_DAYS.WEEKLY,
158
+ [SUBSCRIPTION_PLAN_TYPES.MONTHLY]:
159
+ MIN_SUBSCRIPTION_DURATIONS_DAYS.MONTHLY,
160
+ [SUBSCRIPTION_PLAN_TYPES.YEARLY]:
161
+ MIN_SUBSCRIPTION_DURATIONS_DAYS.YEARLY,
162
+ [SUBSCRIPTION_PLAN_TYPES.UNKNOWN]:
163
+ MIN_SUBSCRIPTION_DURATIONS_DAYS.UNKNOWN,
151
164
  };
152
165
 
153
- const minDuration = minDurations[plan];
166
+ const minDuration = minDurationMap[plan];
154
167
 
155
168
  // If duration is reasonable, trust RevenueCat's date (production)
156
169
  if (durationDays >= minDuration) {
@@ -168,18 +181,20 @@ export function calculateExpirationDate(
168
181
  const calculatedDate = new Date(now);
169
182
 
170
183
  switch (plan) {
171
- case 'weekly':
184
+ case SUBSCRIPTION_PLAN_TYPES.WEEKLY:
172
185
  // Weekly: +7 days
173
- calculatedDate.setDate(calculatedDate.getDate() + 7);
186
+ calculatedDate.setDate(
187
+ calculatedDate.getDate() + SUBSCRIPTION_PERIOD_DAYS.WEEKLY,
188
+ );
174
189
  break;
175
190
 
176
- case 'monthly':
191
+ case SUBSCRIPTION_PLAN_TYPES.MONTHLY:
177
192
  // Monthly: Same day next month
178
193
  // This handles edge cases like Jan 31 → Feb 28/29 correctly
179
194
  calculatedDate.setMonth(calculatedDate.getMonth() + 1);
180
195
  break;
181
196
 
182
- case 'yearly':
197
+ case SUBSCRIPTION_PLAN_TYPES.YEARLY:
183
198
  // Yearly: Same day next year
184
199
  // This handles leap years correctly (Feb 29 → Feb 28/29)
185
200
  calculatedDate.setFullYear(calculatedDate.getFullYear() + 1);
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Period Utilities
3
+ * Subscription period-related helper functions
4
+ *
5
+ * Following SOLID, DRY, KISS principles:
6
+ * - Single Responsibility: Only period-related operations
7
+ * - DRY: No code duplication
8
+ * - KISS: Simple, clear implementations
9
+ */
10
+
11
+ import { SUBSCRIPTION_PERIOD_UNITS } from './subscriptionConstants';
12
+
13
+ /**
14
+ * Get subscription period text from RevenueCat package
15
+ * Extracts readable period text from PurchasesPackage or subscription period object
16
+ *
17
+ * @param input - RevenueCat PurchasesPackage or subscription period object
18
+ * @returns Human-readable period text (e.g., "month", "year", "2 months")
19
+ *
20
+ * @example
21
+ * // From PurchasesPackage
22
+ * getPeriodText(pkg) // Returns: "month"
23
+ *
24
+ * @example
25
+ * // From subscription period object
26
+ * getPeriodText({ unit: "MONTH", numberOfUnits: 1 }) // Returns: "month"
27
+ * getPeriodText({ unit: "YEAR", numberOfUnits: 1 }) // Returns: "year"
28
+ * getPeriodText({ unit: "MONTH", numberOfUnits: 3 }) // Returns: "3 months"
29
+ */
30
+ export function getPeriodText(
31
+ input:
32
+ | {
33
+ product?: {
34
+ subscriptionPeriod?: {
35
+ unit: string;
36
+ numberOfUnits: number;
37
+ } | null;
38
+ };
39
+ }
40
+ | {
41
+ unit: string;
42
+ numberOfUnits: number;
43
+ }
44
+ | null
45
+ | undefined,
46
+ ): string {
47
+ if (!input) return '';
48
+
49
+ // Extract subscription period from PurchasesPackage or use directly
50
+ let subscriptionPeriod:
51
+ | {
52
+ unit: string;
53
+ numberOfUnits: number;
54
+ }
55
+ | null
56
+ | undefined;
57
+
58
+ // Check if input is PurchasesPackage (has product property)
59
+ if ('product' in input && input.product?.subscriptionPeriod) {
60
+ const period = input.product.subscriptionPeriod;
61
+ // Type guard: check if period is an object with unit and numberOfUnits
62
+ if (
63
+ typeof period === 'object' &&
64
+ 'unit' in period &&
65
+ 'numberOfUnits' in period
66
+ ) {
67
+ subscriptionPeriod = {
68
+ unit: period.unit as string,
69
+ numberOfUnits: period.numberOfUnits as number,
70
+ };
71
+ }
72
+ } else if ('unit' in input && 'numberOfUnits' in input) {
73
+ // Input is already a subscription period object
74
+ subscriptionPeriod = {
75
+ unit: input.unit,
76
+ numberOfUnits: input.numberOfUnits,
77
+ };
78
+ }
79
+
80
+ if (!subscriptionPeriod) return '';
81
+
82
+ const { unit, numberOfUnits } = subscriptionPeriod;
83
+
84
+ if (unit === SUBSCRIPTION_PERIOD_UNITS.MONTH) {
85
+ return numberOfUnits === 1 ? 'month' : `${numberOfUnits} months`;
86
+ }
87
+
88
+ if (unit === SUBSCRIPTION_PERIOD_UNITS.YEAR) {
89
+ return numberOfUnits === 1 ? 'year' : `${numberOfUnits} years`;
90
+ }
91
+
92
+ if (unit === SUBSCRIPTION_PERIOD_UNITS.WEEK) {
93
+ return numberOfUnits === 1 ? 'week' : `${numberOfUnits} weeks`;
94
+ }
95
+
96
+ return '';
97
+ }
98
+
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Price Utilities
3
+ * Subscription price-related helper functions
4
+ *
5
+ * Following SOLID, DRY, KISS principles:
6
+ * - Single Responsibility: Only price-related operations
7
+ * - DRY: No code duplication
8
+ * - KISS: Simple, clear implementations
9
+ */
10
+
11
+ import { DATE_CONSTANTS } from './subscriptionConstants';
12
+
13
+ /**
14
+ * Format price for display
15
+ * Formats a price value with currency code using Intl.NumberFormat
16
+ *
17
+ * @param price - Price value (e.g., 9.99)
18
+ * @param currencyCode - ISO 4217 currency code (e.g., "USD", "EUR", "TRY")
19
+ * @returns Formatted price string (e.g., "$9.99", "€9.99", "₺9.99")
20
+ *
21
+ * @example
22
+ * formatPrice(9.99, "USD") // Returns: "$9.99"
23
+ * formatPrice(229.99, "TRY") // Returns: "₺229.99"
24
+ */
25
+ export function formatPrice(price: number, currencyCode: string): string {
26
+ return new Intl.NumberFormat(DATE_CONSTANTS.DEFAULT_LOCALE, {
27
+ style: 'currency',
28
+ currency: currencyCode,
29
+ }).format(price);
30
+ }
31
+
@@ -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
+