@umituz/react-native-subscription 2.15.3 → 2.15.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-subscription",
3
- "version": "2.15.3",
3
+ "version": "2.15.4",
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
@@ -1,15 +1,16 @@
1
1
  /**
2
2
  * Expiration Date Calculator
3
3
  * Handles RevenueCat expiration date extraction with edge case handling
4
+ * Includes sandbox-to-production date conversion for better testing UX
4
5
  */
5
6
 
6
7
  import type { RevenueCatEntitlement } from '../../domain/types/RevenueCatTypes';
7
8
  import { detectPackageType, type SubscriptionPackageType } from '../../../utils/packageTypeDetector';
8
9
 
9
10
  /**
10
- * Add subscription period to a date
11
+ * Add production subscription period to a date
11
12
  */
12
- function addSubscriptionPeriod(date: Date, packageType: SubscriptionPackageType): Date {
13
+ function addProductionPeriod(date: Date, packageType: SubscriptionPackageType): Date {
13
14
  const newDate = new Date(date);
14
15
 
15
16
  switch (packageType) {
@@ -23,7 +24,6 @@ function addSubscriptionPeriod(date: Date, packageType: SubscriptionPackageType)
23
24
  newDate.setDate(newDate.getDate() + 365);
24
25
  break;
25
26
  default:
26
- // Unknown type, default to monthly
27
27
  newDate.setDate(newDate.getDate() + 30);
28
28
  break;
29
29
  }
@@ -32,18 +32,48 @@ function addSubscriptionPeriod(date: Date, packageType: SubscriptionPackageType)
32
32
  }
33
33
 
34
34
  /**
35
- * Adjust expiration date if it equals current date
35
+ * Convert sandbox expiration to production-equivalent expiration
36
36
  *
37
- * RevenueCat sometimes returns expiration date equal to purchase date
38
- * immediately after purchase. This causes false "expired" status.
37
+ * Apple Sandbox durations:
38
+ * - Weekly: 3 minutes 7 days
39
+ * - Monthly: 5 minutes → 30 days
40
+ * - Yearly: 1 hour → 365 days
39
41
  *
40
- * Solution: If expiration date is today or in the past, add the subscription
41
- * period (weekly/monthly/yearly) to prevent false expiration.
42
+ * For better testing UX, we calculate what the expiration would be in production
43
+ * based on the latest purchase date.
44
+ */
45
+ function convertSandboxToProduction(
46
+ latestPurchaseDate: string | null,
47
+ productIdentifier: string
48
+ ): string {
49
+ const purchaseDate = latestPurchaseDate
50
+ ? new Date(latestPurchaseDate)
51
+ : new Date();
52
+
53
+ const packageType = detectPackageType(productIdentifier);
54
+ const productionExpiration = addProductionPeriod(purchaseDate, packageType);
55
+
56
+ return productionExpiration.toISOString();
57
+ }
58
+
59
+ /**
60
+ * Adjust expiration date for edge cases
61
+ *
62
+ * Handles two scenarios:
63
+ * 1. Sandbox mode: Convert to production-equivalent dates for better UX
64
+ * 2. Same-day expiration bug: Add period if expiration is in the past
42
65
  */
43
66
  function adjustExpirationDate(
44
67
  expirationDate: string,
45
- productIdentifier: string
68
+ productIdentifier: string,
69
+ isSandbox: boolean,
70
+ latestPurchaseDate: string | null
46
71
  ): string {
72
+ // In sandbox mode, show production-equivalent expiration for better testing UX
73
+ if (isSandbox && __DEV__) {
74
+ return convertSandboxToProduction(latestPurchaseDate, productIdentifier);
75
+ }
76
+
47
77
  const expDate = new Date(expirationDate);
48
78
  const now = new Date();
49
79
 
@@ -54,7 +84,7 @@ function adjustExpirationDate(
54
84
 
55
85
  // If expiration is today or past, add subscription period
56
86
  const packageType = detectPackageType(productIdentifier);
57
- const adjustedDate = addSubscriptionPeriod(expDate, packageType);
87
+ const adjustedDate = addProductionPeriod(expDate, packageType);
58
88
 
59
89
  return adjustedDate.toISOString();
60
90
  }
@@ -70,9 +100,10 @@ export function getExpirationDate(
70
100
  return null;
71
101
  }
72
102
 
73
- // Apply edge case fix for same-day expirations
74
103
  return adjustExpirationDate(
75
104
  entitlement.expirationDate,
76
- entitlement.productIdentifier
105
+ entitlement.productIdentifier,
106
+ entitlement.isSandbox ?? false,
107
+ entitlement.latestPurchaseDate ?? null
77
108
  );
78
109
  }
@@ -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
- ```
@@ -1,209 +0,0 @@
1
- # Wallet Domain
2
-
3
- Credit balance management, transaction history tracking, and product metadata management.
4
-
5
- ## Location
6
-
7
- **Directory**: `src/domains/wallet/`
8
-
9
- **Type**: Domain
10
-
11
- ## Strategy
12
-
13
- ### Domain Architecture
14
-
15
- The Wallet Domain implements a layered architecture for credit management:
16
-
17
- 1. **Domain Layer**
18
- - Entities: `UserCredits`, `Transaction`, `CreditType`
19
- - Value Objects: `CreditAmount`, `TransactionReason`
20
- - Errors: `InsufficientCreditsError`, `InvalidCreditAmountError`
21
-
22
- 2. **Infrastructure Layer**
23
- - Repositories: `CreditsRepository`, `TransactionsRepository`
24
- - Firebase Services: Data persistence and real-time updates
25
- - Mappers: Entity ↔ DTO transformations
26
-
27
- 3. **Presentation Layer**
28
- - Hooks: `useCredits`, `useDeductCredit`, `useInitializeCredits`
29
- - Components: Credit displays, transaction lists
30
-
31
- ### Credit Flow
32
-
33
- 1. **Initialization**
34
- - Fetch initial credit balance from backend
35
- - Subscribe to real-time credit updates
36
- - Cache in TanStack Query
37
-
38
- 2. **Operations**
39
- - Check balance before operations
40
- - Deduct credits optimistically
41
- - Sync with backend
42
- - Rollback on failure
43
-
44
- 3. **Transaction History**
45
- - Record all credit operations
46
- - Provide audit trail
47
- - Support pagination and filtering
48
-
49
- ### Integration Points
50
-
51
- - **Firebase**: Real database for persistence
52
- - **TanStack Query**: Client-side caching and state management
53
- - **RevenueCat**: Purchase flow integration
54
- - **Presentation Hooks**: UI integration layer
55
-
56
- ## Restrictions
57
-
58
- ### REQUIRED
59
-
60
- - **User Authentication**: All operations require authenticated user
61
- - **Server Validation**: Credit operations MUST be validated server-side
62
- - **Transaction Recording**: All credit changes MUST be recorded in transaction history
63
- - **Error Handling**: MUST handle insufficient credits gracefully
64
-
65
- ### PROHIBITED
66
-
67
- - **NEVER** allow client-side credit modifications without server validation
68
- - **NEVER** deduct credits without sufficient balance
69
- - **DO NOT** expose internal repository logic to UI
70
- - **NEVER** store credit balance in AsyncStorage (use secure backend)
71
-
72
- ### CRITICAL SAFETY
73
-
74
- - **ALWAYS** validate credit amounts (must be positive)
75
- - **ALWAYS** implement optimistic updates with rollback
76
- - **NEVER** trust client-side credit balance for security decisions
77
- - **MUST** implement retry logic for failed operations
78
- - **ALWAYS** sanitize transaction reasons to prevent injection attacks
79
-
80
- ## Rules
81
-
82
- ### Repository Initialization
83
-
84
- ```typescript
85
- // CORRECT - Proper repository setup
86
- const creditsRepository = new CreditsRepository({
87
- firebase: firebaseInstance,
88
- userId: user.uid,
89
- });
90
-
91
- // INCORRECT - Missing userId
92
- const creditsRepository = new CreditsRepository({
93
- firebase: firebaseInstance,
94
- // userId undefined
95
- });
96
- ```
97
-
98
- ### Credit Operations
99
-
100
- ```typescript
101
- // CORRECT - Check before deduct
102
- const hasEnoughCredits = await creditsRepository.checkBalance(requiredAmount);
103
- if (hasEnoughCredits) {
104
- await creditsRepository.deductCredits(requiredAmount, 'feature_usage');
105
- }
106
-
107
- // INCORRECT - Deduct without checking
108
- await creditsRepository.deductCredits(requiredAmount, 'feature_usage');
109
- // May throw InsufficientCreditsError
110
- ```
111
-
112
- ### Transaction Recording
113
-
114
- ```typescript
115
- // CORRECT - Record all operations
116
- await creditsRepository.deductCredits(
117
- amount,
118
- reason,
119
- { featureId, metadata } // Include context
120
- );
121
-
122
- // INCORRECT - Missing context
123
- await creditsRepository.deductCredits(amount, reason);
124
- // Lost audit trail
125
- ```
126
-
127
- ### Error Handling
128
-
129
- ```typescript
130
- // CORRECT - Handle specific errors
131
- try {
132
- await creditsRepository.deductCredits(amount, reason);
133
- } catch (error) {
134
- if (error instanceof InsufficientCreditsError) {
135
- showUpgradePrompt();
136
- } else if (error instanceof InvalidCreditAmountError) {
137
- showInvalidAmountError();
138
- } else {
139
- showGenericError();
140
- }
141
- }
142
-
143
- // INCORRECT - Generic error handling
144
- try {
145
- await creditsRepository.deductCredits(amount, reason);
146
- } catch (error) {
147
- console.error(error); // Doesn't help user
148
- }
149
- ```
150
-
151
- ## AI Agent Guidelines
152
-
153
- ### When Implementing Credit Operations
154
-
155
- 1. **Always** check balance before deducting
156
- 2. **Always** provide transaction reason and metadata
157
- 3. **Always** handle insufficient credits gracefully
158
- 4. **Always** implement optimistic updates with rollback
159
- 5. **Never** trust client-side balance for security
160
-
161
- ### Integration Checklist
162
-
163
- - [ ] Initialize repository with valid userId
164
- - [ ] Implement error boundaries
165
- - [ ] Handle loading states
166
- - [ ] Provide user feedback for all operations
167
- - [ ] Test with insufficient credits
168
- - [ ] Test with zero balance
169
- - [ ] Test transaction history
170
- - [ ] Test real-time updates
171
- - [ ] Implement retry logic
172
- - [ ] Sanitize user inputs
173
-
174
- ### Common Patterns
175
-
176
- 1. **Pre-check**: Always verify balance before operations
177
- 2. **Optimistic Updates**: Update UI immediately, rollback on failure
178
- 3. **Transaction Context**: Include featureId and metadata in all operations
179
- 4. **Error Recovery**: Provide upgrade path when credits insufficient
180
- 5. **Real-time Sync**: Subscribe to credit changes for multi-device sync
181
- 6. **Audit Trail**: Maintain complete transaction history
182
- 7. **Caching**: Use TanStack Query for performance
183
- 8. **Validation**: Validate all inputs on both client and server
184
-
185
- ## Related Documentation
186
-
187
- - **Credits Repository**: `infrastructure/repositories/README.md`
188
- - **useCredits Hook**: `../../presentation/hooks/useCredits.md`
189
- - **useDeductCredit Hook**: `../../presentation/hooks/useDeductCredit.md`
190
- - **UserCredits Entity**: `domain/entities/README.md`
191
- - **Transaction Errors**: `domain/errors/README.md`
192
-
193
- ## Domain Structure
194
-
195
- ```
196
- src/domains/wallet/
197
- ├── domain/
198
- │ ├── entities/ # Core entities
199
- │ │ ├── UserCredits.ts
200
- │ │ └── Transaction.ts
201
- │ ├── value-objects/ # Value objects
202
- │ └── errors/ # Domain errors
203
- ├── infrastructure/
204
- │ ├── repositories/ # Data access
205
- │ └── services/ # External services
206
- └── presentation/
207
- ├── hooks/ # React hooks
208
- └── components/ # UI components
209
- ```