@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.
@@ -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)