@umituz/react-native-subscription 2.14.98 → 2.14.99
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/README.md +0 -1
- package/package.json +1 -1
- package/src/domains/README.md +240 -0
- package/src/domains/config/domain/README.md +390 -0
- package/src/domains/config/domain/entities/README.md +350 -0
- package/src/presentation/hooks/useDeductCredit.md +146 -130
- package/src/revenuecat/application/README.md +158 -0
- package/src/revenuecat/application/ports/README.md +169 -0
- package/src/revenuecat/domain/constants/README.md +183 -0
- package/src/revenuecat/domain/entities/README.md +382 -0
- package/src/revenuecat/domain/types/README.md +373 -0
- package/src/revenuecat/domain/value-objects/README.md +441 -0
- package/src/revenuecat/infrastructure/README.md +50 -0
- package/src/revenuecat/infrastructure/handlers/README.md +218 -0
- package/src/revenuecat/infrastructure/services/README.md +325 -0
- package/src/revenuecat/infrastructure/utils/README.md +382 -0
- package/src/revenuecat/presentation/README.md +184 -0
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
# RevenueCat Infrastructure Services
|
|
2
|
+
|
|
3
|
+
Service implementations for RevenueCat operations.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
This directory contains concrete implementations of RevenueCat interfaces, providing the actual integration with the RevenueCat SDK.
|
|
8
|
+
|
|
9
|
+
## Services
|
|
10
|
+
|
|
11
|
+
### RevenueCatServiceImpl
|
|
12
|
+
|
|
13
|
+
Main service implementation for RevenueCat operations.
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import { Purchases } from '@revenuecat/purchases-capacitor';
|
|
17
|
+
import type { IRevenueCatService } from '../../application/ports/IRevenueCatService';
|
|
18
|
+
|
|
19
|
+
class RevenueCatServiceImpl implements IRevenueCatService {
|
|
20
|
+
// Configuration
|
|
21
|
+
async configure(params: {
|
|
22
|
+
apiKey: string;
|
|
23
|
+
userId?: string;
|
|
24
|
+
observerMode?: boolean;
|
|
25
|
+
}): Promise<void> {
|
|
26
|
+
await Purchases.configure({
|
|
27
|
+
apiKey: params.apiKey,
|
|
28
|
+
appUserID: params.userId,
|
|
29
|
+
observerMode: params.observerMode ?? false,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Purchasing
|
|
34
|
+
async purchasePackage(pkg: Package): Promise<PurchaseResult> {
|
|
35
|
+
return await Purchases.purchasePackage({ aPackage: pkg });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async purchaseProduct(productId: string): Promise<PurchaseResult> {
|
|
39
|
+
return await Purchases.purchaseProduct({ productIdentifier: productId });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async restorePurchases(): Promise<RestoreResult> {
|
|
43
|
+
return await Purchases.restorePurchases();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Customer Information
|
|
47
|
+
async getCustomerInfo(): Promise<CustomerInfo> {
|
|
48
|
+
return await Purchases.getCustomerInfo();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async getCustomerInfoUserId(): Promise<string | null> {
|
|
52
|
+
const info = await this.getCustomerInfo();
|
|
53
|
+
return info.originalAppUserId;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Offerings
|
|
57
|
+
async getOfferings(): Promise<Offerings> {
|
|
58
|
+
return await Purchases.getOfferings();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async getCurrentOffering(): Promise<Offering | null> {
|
|
62
|
+
const offerings = await this.getOfferings();
|
|
63
|
+
return offerings.current;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Entitlements
|
|
67
|
+
async checkEntitlement(entitlementId: string): Promise<boolean> {
|
|
68
|
+
const info = await this.getCustomerInfo();
|
|
69
|
+
return info.entitlements[entitlementId]?.isActive ?? false;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async checkEntitlementInfo(
|
|
73
|
+
entitlementId: string
|
|
74
|
+
): Promise<EntitlementInfo | null> {
|
|
75
|
+
const info = await this.getCustomerInfo();
|
|
76
|
+
return info.entitlements[entitlementId] ?? null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Subscriber Attributes
|
|
80
|
+
async setAttributes(attributes: SubscriberAttributes): Promise<void> {
|
|
81
|
+
await Purchases.setAttributes(attributes);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async setEmail(email: string): Promise<void> {
|
|
85
|
+
await Purchases.setEmail(email);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async setPhoneNumber(phoneNumber: string): Promise<void> {
|
|
89
|
+
await Purchases.setPhoneNumber(phoneNumber);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Log Out
|
|
93
|
+
async logOut(): Promise<void> {
|
|
94
|
+
await Purchases.logOut();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### RevenueCatServiceProvider
|
|
100
|
+
|
|
101
|
+
Provides the RevenueCat service instance.
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
class RevenueCatServiceProvider {
|
|
105
|
+
private static instance: IRevenueCatService | null = null;
|
|
106
|
+
|
|
107
|
+
static getInstance(): IRevenueCatService {
|
|
108
|
+
if (!this.instance) {
|
|
109
|
+
this.instance = new RevenueCatServiceImpl();
|
|
110
|
+
}
|
|
111
|
+
return this.instance;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
static configure(config: {
|
|
115
|
+
apiKey: string;
|
|
116
|
+
userId?: string;
|
|
117
|
+
observerMode?: boolean;
|
|
118
|
+
}): IRevenueCatService {
|
|
119
|
+
const service = this.getInstance();
|
|
120
|
+
service.configure(config);
|
|
121
|
+
return service;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Usage
|
|
127
|
+
|
|
128
|
+
### Initializing the Service
|
|
129
|
+
|
|
130
|
+
```typescript
|
|
131
|
+
import { RevenueCatServiceProvider } from './services/RevenueCatServiceProvider';
|
|
132
|
+
|
|
133
|
+
// Configure RevenueCat
|
|
134
|
+
const service = RevenueCatServiceProvider.configure({
|
|
135
|
+
apiKey: 'your_api_key',
|
|
136
|
+
userId: user?.uid,
|
|
137
|
+
observerMode: false,
|
|
138
|
+
});
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Making Purchases
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
import { RevenueCatServiceProvider } from './services/RevenueCatServiceProvider';
|
|
145
|
+
|
|
146
|
+
async function purchasePremium(userId: string) {
|
|
147
|
+
const service = RevenueCatServiceProvider.getInstance();
|
|
148
|
+
|
|
149
|
+
// Get offerings
|
|
150
|
+
const offerings = await service.getOfferings();
|
|
151
|
+
const monthlyPackage = offerings.current?.monthly;
|
|
152
|
+
|
|
153
|
+
if (!monthlyPackage) {
|
|
154
|
+
throw new Error('No package available');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Purchase
|
|
158
|
+
const result = await service.purchasePackage(monthlyPackage);
|
|
159
|
+
|
|
160
|
+
if (result.error) {
|
|
161
|
+
throw new Error(result.error.message);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Check entitlement
|
|
165
|
+
const hasPremium = await service.checkEntitlement('premium');
|
|
166
|
+
if (hasPremium) {
|
|
167
|
+
console.log('Premium activated!');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return result;
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### Restoring Purchases
|
|
175
|
+
|
|
176
|
+
```typescript
|
|
177
|
+
async function restorePurchases() {
|
|
178
|
+
const service = RevenueCatServiceProvider.getInstance();
|
|
179
|
+
|
|
180
|
+
try {
|
|
181
|
+
const result = await service.restorePurchases();
|
|
182
|
+
|
|
183
|
+
if (result.error) {
|
|
184
|
+
throw new Error(result.error.message);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Update local state
|
|
188
|
+
await updateSubscriptionStatus(result.customerInfo);
|
|
189
|
+
|
|
190
|
+
return result.customerInfo;
|
|
191
|
+
} catch (error) {
|
|
192
|
+
console.error('Restore failed:', error);
|
|
193
|
+
throw error;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### Checking Entitlements
|
|
199
|
+
|
|
200
|
+
```typescript
|
|
201
|
+
async function checkPremiumAccess(): Promise<boolean> {
|
|
202
|
+
const service = RevenueCatServiceProvider.getInstance();
|
|
203
|
+
|
|
204
|
+
try {
|
|
205
|
+
const isActive = await service.checkEntitlement('premium');
|
|
206
|
+
|
|
207
|
+
if (isActive) {
|
|
208
|
+
const entitlement = await service.checkEntitlementInfo('premium');
|
|
209
|
+
console.log('Premium details:', entitlement);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return isActive;
|
|
213
|
+
} catch (error) {
|
|
214
|
+
console.error('Failed to check entitlement:', error);
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### Setting Subscriber Attributes
|
|
221
|
+
|
|
222
|
+
```typescript
|
|
223
|
+
async function updateUserAttributes(user: User) {
|
|
224
|
+
const service = RevenueCatServiceProvider.getInstance();
|
|
225
|
+
|
|
226
|
+
// Set email
|
|
227
|
+
if (user.email) {
|
|
228
|
+
await service.setEmail(user.email);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Set phone number
|
|
232
|
+
if (user.phoneNumber) {
|
|
233
|
+
await service.setPhoneNumber(user.phoneNumber);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Set custom attributes
|
|
237
|
+
await service.setAttributes({
|
|
238
|
+
$displayName: user.displayName,
|
|
239
|
+
account_created_at: user.createdAt.toISOString(),
|
|
240
|
+
subscription_tier: user.tier,
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
## Error Handling
|
|
246
|
+
|
|
247
|
+
### Wrapping Service Calls
|
|
248
|
+
|
|
249
|
+
```typescript
|
|
250
|
+
class RevenueCatServiceWrapper {
|
|
251
|
+
private service: IRevenueCatService;
|
|
252
|
+
|
|
253
|
+
constructor() {
|
|
254
|
+
this.service = RevenueCatServiceProvider.getInstance();
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async safePurchase(
|
|
258
|
+
pkg: Package
|
|
259
|
+
): Promise<PurchaseResult> {
|
|
260
|
+
try {
|
|
261
|
+
return await this.service.purchasePackage(pkg);
|
|
262
|
+
} catch (error) {
|
|
263
|
+
// Convert to domain error
|
|
264
|
+
if (isPurchasesError(error)) {
|
|
265
|
+
return {
|
|
266
|
+
customerInfo: {} as CustomerInfo,
|
|
267
|
+
error,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
throw error;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async safeGetOfferings(): Promise<Offerings | null> {
|
|
275
|
+
try {
|
|
276
|
+
return await this.getOfferings();
|
|
277
|
+
} catch (error) {
|
|
278
|
+
console.error('Failed to get offerings:', error);
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
## Testing
|
|
286
|
+
|
|
287
|
+
### Mock Implementation
|
|
288
|
+
|
|
289
|
+
```typescript
|
|
290
|
+
class MockRevenueCatService implements IRevenueCatService {
|
|
291
|
+
async configure(): Promise<void> {
|
|
292
|
+
console.log('Mock: Configure called');
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
async purchasePackage(pkg: Package): Promise<PurchaseResult> {
|
|
296
|
+
console.log('Mock: Purchase package', pkg.identifier);
|
|
297
|
+
return {
|
|
298
|
+
customerInfo: mockCustomerInfo,
|
|
299
|
+
transaction: mockTransaction,
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// ... other mock implementations
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Use in tests
|
|
307
|
+
const mockService = new MockRevenueCatService();
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
## Best Practices
|
|
311
|
+
|
|
312
|
+
1. **Singleton**: Use singleton pattern for service instance
|
|
313
|
+
2. **Error Handling**: Wrap all service calls in try-catch
|
|
314
|
+
3. **Type Safety**: Use TypeScript types for all operations
|
|
315
|
+
4. **Logging**: Log all RevenueCat operations
|
|
316
|
+
5. **Caching**: Cache customer info and offerings appropriately
|
|
317
|
+
6. **Validation**: Validate parameters before calling SDK
|
|
318
|
+
7. **Configuration**: Configure service only once
|
|
319
|
+
8. **Testing**: Use mock implementations for testing
|
|
320
|
+
|
|
321
|
+
## Related
|
|
322
|
+
|
|
323
|
+
- [RevenueCat Infrastructure](../README.md)
|
|
324
|
+
- [RevenueCat Handlers](../handlers/README.md)
|
|
325
|
+
- [RevenueCat Application Ports](../../application/ports/README.md)
|
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
# RevenueCat Infrastructure Utils
|
|
2
|
+
|
|
3
|
+
Utility functions for RevenueCat operations.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
This directory contains utility functions for common RevenueCat operations including error mapping, data transformation, and validation.
|
|
8
|
+
|
|
9
|
+
## Utilities
|
|
10
|
+
|
|
11
|
+
### Error Mapping
|
|
12
|
+
|
|
13
|
+
Convert RevenueCat SDK errors to domain errors.
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
function mapRevenueCatError(error: PurchasesError): DomainError {
|
|
17
|
+
switch (error.code) {
|
|
18
|
+
case 'PURCHASE_CANCELLED':
|
|
19
|
+
return {
|
|
20
|
+
code: 'PURCHASE_CANCELLED',
|
|
21
|
+
message: 'Purchase was cancelled',
|
|
22
|
+
userMessage: 'You cancelled the purchase',
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
case 'NETWORK_ERROR':
|
|
26
|
+
return {
|
|
27
|
+
code: 'NETWORK_ERROR',
|
|
28
|
+
message: 'Network error occurred',
|
|
29
|
+
userMessage: 'Please check your internet connection',
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
case 'INVALID_CREDENTIALS_ERROR':
|
|
33
|
+
return {
|
|
34
|
+
code: 'CONFIGURATION_ERROR',
|
|
35
|
+
message: 'Invalid RevenueCat credentials',
|
|
36
|
+
userMessage: 'Configuration error. Please contact support.',
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
case 'PRODUCT_NOT_AVAILABLE_FOR_PURCHASE':
|
|
40
|
+
return {
|
|
41
|
+
code: 'PRODUCT_NOT_AVAILABLE',
|
|
42
|
+
message: 'Product not available',
|
|
43
|
+
userMessage: 'This product is currently unavailable',
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
default:
|
|
47
|
+
return {
|
|
48
|
+
code: 'UNKNOWN_ERROR',
|
|
49
|
+
message: error.message || 'Unknown error',
|
|
50
|
+
userMessage: 'An error occurred. Please try again.',
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Entitlement Extraction
|
|
57
|
+
|
|
58
|
+
Extract entitlement information from customer info.
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
function extractEntitlementInfo(
|
|
62
|
+
customerInfo: CustomerInfo,
|
|
63
|
+
entitlementId: string
|
|
64
|
+
): EntitlementInfo | null {
|
|
65
|
+
const entitlement = customerInfo.entitlements[entitlementId];
|
|
66
|
+
|
|
67
|
+
if (!entitlement) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
identifier: entitlementId,
|
|
73
|
+
isActive: entitlement.isActive,
|
|
74
|
+
willRenew: entitlement.willRenew,
|
|
75
|
+
periodType: entitlement.periodType,
|
|
76
|
+
productId: entitlement.productId,
|
|
77
|
+
latestPurchaseDate: entitlement.latestPurchaseDate,
|
|
78
|
+
originalPurchaseDate: entitlement.originalPurchaseDate,
|
|
79
|
+
expirationDate: entitlement.expirationDate,
|
|
80
|
+
renewAt: entitlement.renewAt,
|
|
81
|
+
isSandbox: entitlement.isSandbox,
|
|
82
|
+
billingIssueDetectedAt: entitlement.billingIssueDetectedAt,
|
|
83
|
+
unsubscribeDetectedAt: entitlement.unsubscribeDetectedAt,
|
|
84
|
+
store: entitlement.store,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Package Filtering
|
|
90
|
+
|
|
91
|
+
Filter packages by type or criteria.
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
function filterPackagesByType(
|
|
95
|
+
offering: Offering,
|
|
96
|
+
packageType: PackageType
|
|
97
|
+
): Package[] {
|
|
98
|
+
return offering.availablePackages.filter(
|
|
99
|
+
pkg => pkg.packageType === packageType
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function getSubscriptionPackages(offering: Offering): Package[] {
|
|
104
|
+
return offering.availablePackages.filter(pkg =>
|
|
105
|
+
['monthly', 'annual', 'weekly'].includes(pkg.packageType)
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function getLifetimePackages(offering: Offering): Package[] {
|
|
110
|
+
return offering.availablePackages.filter(pkg =>
|
|
111
|
+
pkg.packageType === 'lifetime'
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function getSinglePurchasePackages(offering: Offering): Package[] {
|
|
116
|
+
return offering.availablePackages.filter(pkg =>
|
|
117
|
+
pkg.packageType === 'single_purchase'
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Price Formatting
|
|
123
|
+
|
|
124
|
+
Format prices for display.
|
|
125
|
+
|
|
126
|
+
```typescript
|
|
127
|
+
function formatPrice(
|
|
128
|
+
price: Price,
|
|
129
|
+
locale?: string
|
|
130
|
+
): string {
|
|
131
|
+
return new Intl.NumberFormat(locale, {
|
|
132
|
+
style: 'currency',
|
|
133
|
+
currency: price.currencyCode,
|
|
134
|
+
}).format(price.amount);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function formatPricePerMonth(
|
|
138
|
+
package: Package,
|
|
139
|
+
locale?: string
|
|
140
|
+
): string {
|
|
141
|
+
const { price, product } = package;
|
|
142
|
+
|
|
143
|
+
if (product.subscriptionPeriod) {
|
|
144
|
+
const { value, unit } = product.subscriptionPeriod;
|
|
145
|
+
|
|
146
|
+
// Calculate monthly equivalent
|
|
147
|
+
let months = 1;
|
|
148
|
+
if (unit === 'week') months = value / 4;
|
|
149
|
+
if (unit === 'month') months = value;
|
|
150
|
+
if (unit === 'year') months = value * 12;
|
|
151
|
+
|
|
152
|
+
const monthlyPrice = price.amount / months;
|
|
153
|
+
return formatPrice(
|
|
154
|
+
{ amount: monthlyPrice, currencyCode: price.currencyCode },
|
|
155
|
+
locale
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return formatPrice(price, locale);
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Period Formatting
|
|
164
|
+
|
|
165
|
+
Format subscription periods.
|
|
166
|
+
|
|
167
|
+
```typescript
|
|
168
|
+
function formatPeriod(
|
|
169
|
+
period: SubscriptionPeriod,
|
|
170
|
+
locale = 'en-US'
|
|
171
|
+
): string {
|
|
172
|
+
const formatter = new Intl.RelativeTimeFormat(locale, {
|
|
173
|
+
numeric: 'always',
|
|
174
|
+
});
|
|
175
|
+
return formatter.format(period.value, period.unit);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function getPeriodInMonths(period: SubscriptionPeriod): number {
|
|
179
|
+
switch (period.unit) {
|
|
180
|
+
case 'day': return period.value / 30;
|
|
181
|
+
case 'week': return period.value / 4;
|
|
182
|
+
case 'month': return period.value;
|
|
183
|
+
case 'year': return period.value * 12;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function getPeriodInDays(period: SubscriptionPeriod): number {
|
|
188
|
+
switch (period.unit) {
|
|
189
|
+
case 'day': return period.value;
|
|
190
|
+
case 'week': return period.value * 7;
|
|
191
|
+
case 'month': return period.value * 30;
|
|
192
|
+
case 'year': return period.value * 365;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### Subscription Status
|
|
198
|
+
|
|
199
|
+
Determine subscription status from entitlement.
|
|
200
|
+
|
|
201
|
+
```typescript
|
|
202
|
+
function getSubscriptionStatus(
|
|
203
|
+
entitlement: EntitlementInfo | null
|
|
204
|
+
): SubscriptionStatus {
|
|
205
|
+
if (!entitlement || !entitlement.isActive) {
|
|
206
|
+
return 'expired';
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (entitlement.billingIssueDetectedAt) {
|
|
210
|
+
return 'in_billing_retry';
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (entitlement.unsubscribeDetectedAt && !entitlement.willRenew) {
|
|
214
|
+
return 'cancelled';
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return 'active';
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function getSubscriptionStatusType(
|
|
221
|
+
entitlement: EntitlementInfo | null
|
|
222
|
+
): 'active' | 'expired' | 'canceled' | 'none' {
|
|
223
|
+
if (!entitlement) {
|
|
224
|
+
return 'none';
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (!entitlement.isActive) {
|
|
228
|
+
return 'expired';
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (entitlement.unsubscribeDetectedAt && !entitlement.willRenew) {
|
|
232
|
+
return 'canceled';
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return 'active';
|
|
236
|
+
}
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### Validation
|
|
240
|
+
|
|
241
|
+
Validate RevenueCat data.
|
|
242
|
+
|
|
243
|
+
```typescript
|
|
244
|
+
function isValidEntitlementId(id: string): boolean {
|
|
245
|
+
const validIds = ['premium', 'pro', 'lifetime'];
|
|
246
|
+
return validIds.includes(id);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function isValidOffering(offering: Offering | null): boolean {
|
|
250
|
+
return (
|
|
251
|
+
offering !== null &&
|
|
252
|
+
offering.availablePackages.length > 0
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function isValidPackage(pkg: Package | null): boolean {
|
|
257
|
+
return (
|
|
258
|
+
pkg !== null &&
|
|
259
|
+
!!pkg.identifier &&
|
|
260
|
+
!!pkg.product &&
|
|
261
|
+
!!pkg.price
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
### Debug Helpers
|
|
267
|
+
|
|
268
|
+
Helper functions for debugging.
|
|
269
|
+
|
|
270
|
+
```typescript
|
|
271
|
+
function logCustomerInfo(info: CustomerInfo): void {
|
|
272
|
+
if (__DEV__) {
|
|
273
|
+
console.log('[RevenueCat] Customer Info:', {
|
|
274
|
+
userId: info.originalAppUserId,
|
|
275
|
+
activeSubscriptions: info.activeSubscriptions,
|
|
276
|
+
allPurchasedProductIds: info.allPurchasedProductIds,
|
|
277
|
+
entitlements: Object.keys(info.entitlements),
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function logPurchaseResult(result: PurchaseResult): void {
|
|
283
|
+
if (__DEV__) {
|
|
284
|
+
console.log('[RevenueCat] Purchase Result:', {
|
|
285
|
+
transactionId: result.transaction?.transactionIdentifier,
|
|
286
|
+
productId: result.transaction?.productIdentifier,
|
|
287
|
+
hasPremium: !!result.customerInfo.entitlements.premium?.isActive,
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function logError(error: PurchasesError): void {
|
|
293
|
+
if (__DEV__) {
|
|
294
|
+
console.error('[RevenueCat] Error:', {
|
|
295
|
+
code: error.code,
|
|
296
|
+
message: error.message,
|
|
297
|
+
readableMessage: error.readableErrorMessage,
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
## Usage Examples
|
|
304
|
+
|
|
305
|
+
### Using Error Mapping
|
|
306
|
+
|
|
307
|
+
```typescript
|
|
308
|
+
import { mapRevenueCatError } from './utils';
|
|
309
|
+
|
|
310
|
+
try {
|
|
311
|
+
const result = await purchasePackage(pkg);
|
|
312
|
+
} catch (error) {
|
|
313
|
+
const domainError = mapRevenueCatError(error);
|
|
314
|
+
|
|
315
|
+
// Show user-friendly message
|
|
316
|
+
Alert.alert('Error', domainError.userMessage);
|
|
317
|
+
|
|
318
|
+
// Log technical details
|
|
319
|
+
console.error(domainError.message);
|
|
320
|
+
}
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
### Extracting Entitlement Info
|
|
324
|
+
|
|
325
|
+
```typescript
|
|
326
|
+
import { extractEntitlementInfo } from './utils';
|
|
327
|
+
|
|
328
|
+
const customerInfo = await getCustomerInfo();
|
|
329
|
+
const premium = extractEntitlementInfo(customerInfo, 'premium');
|
|
330
|
+
|
|
331
|
+
if (premium?.isActive) {
|
|
332
|
+
console.log('Premium active until', premium.expirationDate);
|
|
333
|
+
}
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
### Filtering Packages
|
|
337
|
+
|
|
338
|
+
```typescript
|
|
339
|
+
import { filterPackagesByType, getSubscriptionPackages } from './utils';
|
|
340
|
+
|
|
341
|
+
const offering = await getOfferings();
|
|
342
|
+
const monthlyPackages = filterPackagesByType(offering.current, 'monthly');
|
|
343
|
+
const allSubscriptions = getSubscriptionPackages(offering.current);
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
### Formatting Prices
|
|
347
|
+
|
|
348
|
+
```typescript
|
|
349
|
+
import { formatPrice, formatPricePerMonth } from './utils';
|
|
350
|
+
|
|
351
|
+
// Standard price
|
|
352
|
+
const priceString = formatPrice(pkg.price, 'en-US'); // '$9.99'
|
|
353
|
+
const priceStringTR = formatPrice(pkg.price, 'tr-TR'); // '9,99 $'
|
|
354
|
+
|
|
355
|
+
// Per month (for annual)
|
|
356
|
+
const perMonth = formatPricePerMonth(annualPackage, 'en-US'); // '$8.33/mo'
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
### Getting Subscription Status
|
|
360
|
+
|
|
361
|
+
```typescript
|
|
362
|
+
import { getSubscriptionStatus, getSubscriptionStatusType } from './utils';
|
|
363
|
+
|
|
364
|
+
const entitlement = customerInfo.entitlements.premium;
|
|
365
|
+
const status = getSubscriptionStatus(entitlement); // 'active'
|
|
366
|
+
const statusType = getSubscriptionStatusType(entitlement); // 'active'
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
## Best Practices
|
|
370
|
+
|
|
371
|
+
1. **Error Handling**: Always use error mapping for user-facing messages
|
|
372
|
+
2. **Type Safety**: Ensure types are validated before use
|
|
373
|
+
3. **Locale**: Respect user locale for formatting
|
|
374
|
+
4. **Null Checks**: Always check for null/undefined values
|
|
375
|
+
5. **Logging**: Use debug helpers in development
|
|
376
|
+
6. **Validation**: Validate data before processing
|
|
377
|
+
|
|
378
|
+
## Related
|
|
379
|
+
|
|
380
|
+
- [RevenueCat Infrastructure](../README.md)
|
|
381
|
+
- [RevenueCat Domain Types](../../domain/types/README.md)
|
|
382
|
+
- [RevenueCat Errors](../../domain/errors/README.md)
|