@umituz/react-native-subscription 2.15.3 → 2.15.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": "2.15.3",
3
+ "version": "2.15.5",
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,6 +16,7 @@ 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";
19
20
  import type {
20
21
  SubscriptionSettingsConfig,
21
22
  SubscriptionStatusType,
@@ -75,8 +76,11 @@ export const useSubscriptionSettingsConfig = (
75
76
  }, [premiumEntitlement?.productIdentifier, creditLimit]);
76
77
 
77
78
  // Get expiration date from RevenueCat entitlement (source of truth)
78
- // premiumEntitlement.expirationDate is an ISO string from RevenueCat
79
- const entitlementExpirationDate = premiumEntitlement?.expirationDate || null;
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]);
80
84
 
81
85
  // Prefer CustomerInfo expiration (real-time) over cached status
82
86
  const expiresAtIso = entitlementExpirationDate || (statusExpirationDate
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Sandbox Duration Configuration
3
+ * Apple Sandbox subscription durations vs Production durations
4
+ *
5
+ * @see https://developer.apple.com/documentation/storekit/in-app_purchase/testing_in-app_purchases_with_sandbox
6
+ */
7
+
8
+ import type { SubscriptionPackageType } from '../../../utils/packageTypeDetector';
9
+
10
+ /**
11
+ * Apple Sandbox subscription durations in minutes
12
+ */
13
+ export const SANDBOX_DURATIONS: Record<SubscriptionPackageType, number> = {
14
+ weekly: 3,
15
+ monthly: 5,
16
+ yearly: 60,
17
+ unknown: 5,
18
+ };
19
+
20
+ /**
21
+ * Production subscription durations in days
22
+ */
23
+ export const PRODUCTION_DURATIONS: Record<SubscriptionPackageType, number> = {
24
+ weekly: 7,
25
+ monthly: 30,
26
+ yearly: 365,
27
+ unknown: 30,
28
+ };
29
+
30
+ /**
31
+ * Get production duration in days for a package type
32
+ */
33
+ export function getProductionDurationDays(packageType: SubscriptionPackageType): number {
34
+ return PRODUCTION_DURATIONS[packageType] ?? PRODUCTION_DURATIONS.unknown;
35
+ }
36
+
37
+ /**
38
+ * Get sandbox duration in minutes for a package type
39
+ */
40
+ export function getSandboxDurationMinutes(packageType: SubscriptionPackageType): number {
41
+ return SANDBOX_DURATIONS[packageType] ?? SANDBOX_DURATIONS.unknown;
42
+ }
@@ -1,78 +1,37 @@
1
1
  /**
2
2
  * Expiration Date Calculator
3
- * Handles RevenueCat expiration date extraction with edge case handling
3
+ * Main entry point for calculating subscription expiration dates
4
4
  */
5
5
 
6
6
  import type { RevenueCatEntitlement } from '../../domain/types/RevenueCatTypes';
7
- import { detectPackageType, type SubscriptionPackageType } from '../../../utils/packageTypeDetector';
7
+ import {
8
+ shouldConvertSandbox,
9
+ convertToProductionExpiration,
10
+ adjustPastExpiration,
11
+ } from './SandboxDurationConverter';
8
12
 
9
13
  /**
10
- * Add subscription period to a date
11
- */
12
- function addSubscriptionPeriod(date: Date, packageType: SubscriptionPackageType): Date {
13
- const newDate = new Date(date);
14
-
15
- switch (packageType) {
16
- case 'weekly':
17
- newDate.setDate(newDate.getDate() + 7);
18
- break;
19
- case 'monthly':
20
- newDate.setDate(newDate.getDate() + 30);
21
- break;
22
- case 'yearly':
23
- newDate.setDate(newDate.getDate() + 365);
24
- break;
25
- default:
26
- // Unknown type, default to monthly
27
- newDate.setDate(newDate.getDate() + 30);
28
- break;
29
- }
30
-
31
- return newDate;
32
- }
33
-
34
- /**
35
- * Adjust expiration date if it equals current date
36
- *
37
- * RevenueCat sometimes returns expiration date equal to purchase date
38
- * immediately after purchase. This causes false "expired" status.
14
+ * Get adjusted expiration date from entitlement
39
15
  *
40
- * Solution: If expiration date is today or in the past, add the subscription
41
- * period (weekly/monthly/yearly) to prevent false expiration.
16
+ * Handles:
17
+ * 1. Sandbox mode: Converts to production-equivalent dates for better UX
18
+ * 2. Past expiration: Adds subscription period if expiration is in the past
42
19
  */
43
- function adjustExpirationDate(
44
- expirationDate: string,
45
- productIdentifier: string
46
- ): string {
47
- const expDate = new Date(expirationDate);
48
- const now = new Date();
49
-
50
- // If expiration is in the future, no adjustment needed
51
- if (expDate > now) {
52
- return expirationDate;
53
- }
54
-
55
- // If expiration is today or past, add subscription period
56
- const packageType = detectPackageType(productIdentifier);
57
- const adjustedDate = addSubscriptionPeriod(expDate, packageType);
58
-
59
- return adjustedDate.toISOString();
60
- }
61
-
62
20
  export function getExpirationDate(
63
21
  entitlement: RevenueCatEntitlement | null
64
22
  ): string | null {
65
- if (!entitlement) {
23
+ if (!entitlement?.expirationDate) {
66
24
  return null;
67
25
  }
68
26
 
69
- if (!entitlement.expirationDate) {
70
- return null;
27
+ const { expirationDate, productIdentifier, isSandbox, latestPurchaseDate } = entitlement;
28
+
29
+ if (shouldConvertSandbox(isSandbox)) {
30
+ return convertToProductionExpiration(latestPurchaseDate, productIdentifier);
71
31
  }
72
32
 
73
- // Apply edge case fix for same-day expirations
74
- return adjustExpirationDate(
75
- entitlement.expirationDate,
76
- entitlement.productIdentifier
77
- );
33
+ const expDate = new Date(expirationDate);
34
+ const adjustedDate = adjustPastExpiration(expDate, productIdentifier);
35
+
36
+ return adjustedDate.toISOString();
78
37
  }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Sandbox Duration Converter
3
+ * Converts Apple Sandbox subscription durations to production-equivalent dates
4
+ */
5
+
6
+ import { detectPackageType, type SubscriptionPackageType } from '../../../utils/packageTypeDetector';
7
+ import { getProductionDurationDays } from '../config/SandboxDurationConfig';
8
+
9
+ /**
10
+ * Add production 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
+ }
21
+
22
+ /**
23
+ * Check if sandbox conversion should be applied
24
+ * Only applies in DEV mode with sandbox flag
25
+ */
26
+ export function shouldConvertSandbox(isSandbox: boolean): boolean {
27
+ return isSandbox && __DEV__;
28
+ }
29
+
30
+ /**
31
+ * Convert sandbox expiration to production-equivalent expiration
32
+ *
33
+ * In Apple Sandbox:
34
+ * - Weekly subscriptions expire in 3 minutes
35
+ * - Monthly subscriptions expire in 5 minutes
36
+ * - Yearly subscriptions expire in 1 hour
37
+ *
38
+ * For better testing UX, we calculate the production-equivalent expiration
39
+ * based on the purchase date + production duration.
40
+ */
41
+ export function convertToProductionExpiration(
42
+ purchaseDate: string | null,
43
+ productIdentifier: string
44
+ ): string {
45
+ const basePurchaseDate = purchaseDate ? new Date(purchaseDate) : new Date();
46
+ const packageType = detectPackageType(productIdentifier);
47
+ const productionExpiration = addProductionPeriod(basePurchaseDate, packageType);
48
+
49
+ return productionExpiration.toISOString();
50
+ }
51
+
52
+ /**
53
+ * Adjust expiration date if it's in the past
54
+ * Adds subscription period to prevent false expiration
55
+ */
56
+ export function adjustPastExpiration(
57
+ expirationDate: Date,
58
+ productIdentifier: string
59
+ ): Date {
60
+ const now = new Date();
61
+
62
+ if (expirationDate > now) {
63
+ return expirationDate;
64
+ }
65
+
66
+ const packageType = detectPackageType(productIdentifier);
67
+ return addProductionPeriod(expirationDate, packageType);
68
+ }
@@ -1,274 +0,0 @@
1
- # Domains
2
-
3
- Specialized domain modules implementing specific business logic and features.
4
-
5
- ## Location
6
-
7
- **Directory**: `src/domains/`
8
-
9
- **Type**: Domain Collection
10
-
11
- ## Strategy
12
-
13
- ### Domain-Driven Design
14
-
15
- This directory implements Domain-Driven Design (DDD) principles:
16
-
17
- 1. **Wallet Domain**: Credit balance, transactions, and purchase flow
18
- 2. **Paywall Domain**: Upgrade prompts, subscription management
19
- 3. **Config Domain**: Feature flags, subscription configuration
20
-
21
- Each domain is self-contained with:
22
- - **Domain Layer**: Business logic, entities, value objects
23
- - **Infrastructure Layer**: External integrations, repositories
24
- - **Presentation Layer**: Domain-specific hooks and components
25
-
26
- ### Architecture Pattern
27
-
28
- ```
29
- ┌─────────────────────────────────────┐
30
- │ Application Layer │
31
- │ (Use Cases, Orchestration) │
32
- └──────────────┬──────────────────────┘
33
-
34
- ┌───────┴────────┐
35
- │ │
36
- ┌──────▼──────┐ ┌─────▼──────┐
37
- │ Wallet │ │ Paywall │
38
- │ Domain │ │ Domain │
39
- ├─────────────┤ ├────────────┤
40
- │ Domain │ │ Domain │
41
- │ Infra │ │ Infra │
42
- │ Presentation│ │ Presentation│
43
- └──────┬──────┘ └─────┬──────┘
44
- │ │
45
- └────────┬───────┘
46
-
47
- ┌────────▼──────────┐
48
- │ Shared Infra │
49
- │ (Firebase, etc) │
50
- └───────────────────┘
51
- ```
52
-
53
- ## Domain Modules
54
-
55
- ### Wallet Domain (`wallet/`)
56
-
57
- **Responsibility**: Credit balance and transaction management
58
-
59
- **Key Features**:
60
- - Credit balance tracking
61
- - Transaction history
62
- - Purchase initialization
63
- - Real-time updates
64
-
65
- **Documentation**: `wallet/README.md`
66
-
67
- ### Paywall Domain (`paywall/`)
68
-
69
- **Responsibility**: Subscription upgrade flows and paywall UI
70
-
71
- **Key Features**:
72
- - Paywall display logic
73
- - Subscription management
74
- - Feature gating
75
- - Upgrade prompts
76
-
77
- **Documentation**: `paywall/README.md`
78
-
79
- ### Config Domain (`config/`)
80
-
81
- **Responsibility**: Subscription configuration and feature flags
82
-
83
- **Key Features**:
84
- - Subscription tiers
85
- - Feature configuration
86
- - Pricing rules
87
- - Feature flags
88
-
89
- **Documentation**: `config/README.md`
90
-
91
- ## Restrictions
92
-
93
- ### REQUIRED
94
-
95
- - **Domain Isolation**: Domains MUST NOT directly depend on each other
96
- - **Interface Segregation**: Use well-defined interfaces between layers
97
- - **Dependency Inversion**: Depend on abstractions, not concretions
98
- - **Testability**: All domains MUST be testable in isolation
99
-
100
- ### PROHIBITED
101
-
102
- - **NEVER** share domain logic between domains (use shared kernel if needed)
103
- - **NEVER** create circular dependencies between domains
104
- - **DO NOT** bypass domain layer from presentation
105
- - **NEVER** expose infrastructure details to other domains
106
-
107
- ### CRITICAL SAFETY
108
-
109
- - **ALWAYS** validate invariants at domain boundaries
110
- - **ALWAYS** implement domain errors for business rule violations
111
- - **NEVER** allow inconsistent domain state
112
- - **MUST** implement proper transaction boundaries
113
- - **ALWAYS** sanitize inputs from external sources
114
-
115
- ## Rules
116
-
117
- ### Domain Boundaries
118
-
119
- ```typescript
120
- // CORRECT - Respecting domain boundaries
121
- // Wallet domain handles credits
122
- const { credits } = useCredits(); // From wallet domain
123
-
124
- // Paywall domain handles upgrades
125
- const { showPaywall } = usePaywallOperations(); // From paywall domain
126
-
127
- // INCORRECT - Crossing domain boundaries
128
- const walletRepository = new WalletRepository();
129
- // Directly using wallet repo in paywall component
130
- ```
131
-
132
- ### Domain Errors
133
-
134
- ```typescript
135
- // CORRECT - Domain-specific errors
136
- class InsufficientCreditsError extends DomainError {
137
- constructor(required: number, available: number) {
138
- super(`Insufficient credits: need ${required}, have ${available}`);
139
- }
140
- }
141
-
142
- // INCORRECT - Generic errors
143
- throw new Error('Not enough credits'); // Loses domain context
144
- ```
145
-
146
- ### Dependency Direction
147
-
148
- ```typescript
149
- // CORRECT - Dependency inversion
150
- interface ICreditsRepository {
151
- getBalance(userId: string): Promise<number>;
152
- deductCredits(userId: string, amount: number): Promise<void>;
153
- }
154
-
155
- // Domain depends on interface, not implementation
156
- class CreditService {
157
- constructor(private repo: ICreditsRepository) {}
158
- }
159
-
160
- // INCORRECT - Concrete dependency
161
- class CreditService {
162
- constructor(private repo: FirebaseCreditsRepository) {}
163
- // Tightly coupled to Firebase
164
- }
165
- ```
166
-
167
- ## AI Agent Guidelines
168
-
169
- ### When Working with Domains
170
-
171
- 1. **Always** respect domain boundaries
172
- 2. **Always** use dependency inversion
173
- 3. **Always** implement domain-specific errors
174
- 4. **Always** validate invariants at boundaries
175
- 5. **Never** create circular dependencies
176
-
177
- ### Integration Checklist
178
-
179
- - [ ] Identify correct domain for feature
180
- - [ ] Respect domain boundaries
181
- - [ ] Use appropriate interfaces
182
- - [ ] Handle domain errors
183
- - [ ] Test domain in isolation
184
- - [ ] Document domain interactions
185
- - [ ] Validate invariants
186
- - [ ] Implement transaction boundaries
187
- - [ ] Test cross-domain scenarios
188
- - [ ] Verify no circular dependencies
189
-
190
- ### Common Patterns
191
-
192
- 1. **Aggregate Root**: Single entry point for aggregate
193
- 2. **Value Objects**: Immutable values with no identity
194
- 3. **Domain Events**: Publish domain events for side effects
195
- 4. **Repositories**: Abstract data access
196
- 5. **Factories**: Complex object creation
197
- 6. **Domain Services**: Business logic that doesn't fit entities
198
- 7. **Specification**: Business rule encapsulation
199
- 8. **Anti-Corruption Layer**: Isolate from external systems
200
-
201
- ## Related Documentation
202
-
203
- - **Wallet Domain**: `wallet/README.md`
204
- - **Paywall Domain**: `paywall/README.md`
205
- - **Config Domain**: `config/README.md`
206
- - **Domain Layer**: `../domain/README.md`
207
- - **Infrastructure**: `../infrastructure/README.md`
208
-
209
- ## Domain Structure
210
-
211
- ```
212
- src/domains/
213
- ├── wallet/ # Wallet and credits domain
214
- │ ├── domain/ # Business logic
215
- │ ├── infrastructure/ # External integrations
216
- │ └── presentation/ # UI hooks and components
217
- ├── paywall/ # Paywall and upgrades domain
218
- │ ├── domain/
219
- │ ├── infrastructure/
220
- │ └── presentation/
221
- └── config/ # Configuration domain
222
- ├── domain/
223
- ├── infrastructure/
224
- └── presentation/
225
- ```
226
-
227
- ## Creating a New Domain
228
-
229
- When creating a new domain:
230
-
231
- 1. **Define Boundaries**: What is the domain's responsibility?
232
- 2. **Identify Entities**: What are the core business objects?
233
- 3. **Define Invariants**: What rules must always be true?
234
- 4. **Design Interfaces**: How will other layers interact?
235
- 5. **Implement Repository**: Abstract data access
236
- 6. **Create Presentation Layer**: Hooks and components
237
- 7. **Write Tests**: Test domain logic in isolation
238
- 8. **Document**: Provide comprehensive README
239
-
240
- Example:
241
-
242
- ```typescript
243
- // Domain entity
244
- export class FeatureFlag {
245
- constructor(
246
- public readonly id: string,
247
- public readonly name: string,
248
- private _isEnabled: boolean
249
- ) {}
250
-
251
- get isEnabled(): boolean {
252
- return this._isEnabled;
253
- }
254
-
255
- enable(): void {
256
- this._isEnabled = true;
257
- }
258
-
259
- disable(): void {
260
- this._isEnabled = false;
261
- }
262
- }
263
-
264
- // Repository interface
265
- export interface IFeatureFlagRepository {
266
- findById(id: string): Promise<FeatureFlag | null>;
267
- save(flag: FeatureFlag): Promise<void>;
268
- }
269
-
270
- // Presentation hook
271
- export function useFeatureFlag(featureId: string) {
272
- // Hook implementation
273
- }
274
- ```