@umituz/react-native-subscription 2.27.34 → 2.27.35

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.27.34",
3
+ "version": "2.27.35",
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",
@@ -20,9 +20,18 @@ export interface FirebaseAuthLike {
20
20
  export interface CreditPackageConfig { identifierPattern?: string; amounts?: Record<string, number>; }
21
21
 
22
22
  export interface SubscriptionInitConfig {
23
- apiKey?: string; apiKeyIos?: string; apiKeyAndroid?: string; testStoreKey?: string; entitlementId: string; credits: CreditsConfig;
24
- getAnonymousUserId: () => Promise<string>; getFirebaseAuth: () => FirebaseAuthLike | null; showAuthModal: () => void;
25
- onCreditsUpdated?: (userId: string) => void; creditPackages?: CreditPackageConfig; timeoutMs?: number; authStateTimeoutMs?: number;
23
+ apiKey?: string;
24
+ apiKeyIos?: string;
25
+ apiKeyAndroid?: string;
26
+ entitlementId: string;
27
+ credits: CreditsConfig;
28
+ getAnonymousUserId: () => Promise<string>;
29
+ getFirebaseAuth: () => FirebaseAuthLike | null;
30
+ showAuthModal: () => void;
31
+ onCreditsUpdated?: (userId: string) => void;
32
+ creditPackages?: CreditPackageConfig;
33
+ timeoutMs?: number;
34
+ authStateTimeoutMs?: number;
26
35
  }
27
36
 
28
37
  const waitForAuthState = async (getAuth: () => FirebaseAuthLike | null, timeoutMs: number): Promise<string | undefined> => {
@@ -36,27 +45,14 @@ const waitForAuthState = async (getAuth: () => FirebaseAuthLike | null, timeoutM
36
45
  };
37
46
 
38
47
  export const initializeSubscription = async (config: SubscriptionInitConfig): Promise<void> => {
39
- const { apiKey, apiKeyIos, apiKeyAndroid, testStoreKey, entitlementId, credits, getAnonymousUserId, getFirebaseAuth, showAuthModal, onCreditsUpdated, creditPackages, timeoutMs = 10000, authStateTimeoutMs = 2000 } = config;
48
+ const {
49
+ apiKey, apiKeyIos, apiKeyAndroid, entitlementId, credits,
50
+ getAnonymousUserId, getFirebaseAuth, showAuthModal,
51
+ onCreditsUpdated, creditPackages, timeoutMs = 10000, authStateTimeoutMs = 2000,
52
+ } = config;
40
53
 
41
- if (__DEV__) {
42
- console.log('[DEBUG initializeSubscription] Config received:', {
43
- hasApiKey: !!apiKey,
44
- hasApiKeyIos: !!apiKeyIos,
45
- hasApiKeyAndroid: !!apiKeyAndroid,
46
- hasTestStoreKey: !!testStoreKey,
47
- apiKeyPrefix: apiKey?.substring(0, 10),
48
- testStoreKeyPrefix: testStoreKey?.substring(0, 10),
49
- platform: Platform.OS,
50
- });
51
- }
52
-
53
- const key = Platform.OS === "ios" ? (apiKeyIos || apiKey || "") : (apiKeyAndroid || apiKey || "");
54
-
55
- if (__DEV__) {
56
- console.log('[DEBUG initializeSubscription] Resolved key:', key ? key.substring(0, 10) + '...' : 'empty');
57
- }
58
-
59
- if (!key) throw new Error("API key required");
54
+ const key = Platform.OS === 'ios' ? (apiKeyIos || apiKey || '') : (apiKeyAndroid || apiKey || '');
55
+ if (!key) throw new Error('API key required');
60
56
 
61
57
  configureCreditsRepository({ ...credits, creditPackageAmounts: creditPackages?.amounts });
62
58
 
@@ -205,21 +201,11 @@ export const initializeSubscription = async (config: SubscriptionInitConfig): Pr
205
201
  }
206
202
  };
207
203
 
208
- if (__DEV__) {
209
- console.log('[DEBUG initializeSubscription] Configuring SubscriptionManager with:', {
210
- apiKeyPrefix: key.substring(0, 10),
211
- hasTestStoreKey: !!testStoreKey,
212
- testStoreKeyPrefix: testStoreKey?.substring(0, 10),
213
- entitlementId,
214
- });
215
- }
216
-
217
204
  SubscriptionManager.configure({
218
205
  config: {
219
206
  apiKey: key,
220
- testStoreKey,
221
207
  entitlementIdentifier: entitlementId,
222
- consumableProductIdentifiers: [creditPackages?.identifierPattern || "credit"],
208
+ consumableProductIdentifiers: [creditPackages?.identifierPattern || 'credit'],
223
209
  onPurchaseCompleted: onPurchase,
224
210
  onRenewalDetected: onRenewal,
225
211
  onPremiumStatusChanged,
@@ -1,76 +1,16 @@
1
- /**
2
- * Subscription Init Module Factory
3
- * Creates a ready-to-use InitModule for app initialization
4
- */
5
-
6
1
  import type { InitModule } from '@umituz/react-native-design-system';
7
2
  import { initializeSubscription, type SubscriptionInitConfig } from '../infrastructure/services/SubscriptionInitializer';
8
3
 
9
4
  declare const __DEV__: boolean;
10
5
 
11
6
  export interface SubscriptionInitModuleConfig extends Omit<SubscriptionInitConfig, 'apiKey'> {
12
- /**
13
- * RevenueCat API key getter function
14
- * Returns the API key or undefined if not available
15
- */
16
7
  getApiKey: () => string | undefined;
17
-
18
- /**
19
- * Optional RevenueCat test store key getter
20
- */
21
- getTestStoreKey?: () => string | undefined;
22
-
23
- /**
24
- * Whether this module is critical for app startup
25
- * @default true
26
- */
27
8
  critical?: boolean;
28
-
29
- /**
30
- * Module dependencies
31
- * @default ["auth"]
32
- */
33
9
  dependsOn?: string[];
34
10
  }
35
11
 
36
- /**
37
- * Creates a Subscription initialization module for use with createAppInitializer
38
- *
39
- * @example
40
- * ```typescript
41
- * import { createAppInitializer } from "@umituz/react-native-design-system";
42
- * import { createFirebaseInitModule } from "@umituz/react-native-firebase";
43
- * import { createAuthInitModule } from "@umituz/react-native-auth";
44
- * import { createSubscriptionInitModule } from "@umituz/react-native-subscription";
45
- *
46
- * export const initializeApp = createAppInitializer({
47
- * modules: [
48
- * createFirebaseInitModule(),
49
- * createAuthInitModule({ userCollection: "users" }),
50
- * createSubscriptionInitModule({
51
- * getApiKey: () => getRevenueCatApiKey(),
52
- * entitlementId: "premium",
53
- * credits: {
54
- * collectionName: "credits",
55
- * creditLimit: 500,
56
- * enableFreeCredits: true,
57
- * freeCredits: 1,
58
- * },
59
- * }),
60
- * ],
61
- * });
62
- * ```
63
- */
64
- export function createSubscriptionInitModule(
65
- config: SubscriptionInitModuleConfig
66
- ): InitModule {
67
- const {
68
- getApiKey,
69
- getTestStoreKey,
70
- critical = true,
71
- dependsOn = ['auth'],
72
- ...subscriptionConfig
73
- } = config;
12
+ export function createSubscriptionInitModule(config: SubscriptionInitModuleConfig): InitModule {
13
+ const { getApiKey, critical = true, dependsOn = ['auth'], ...subscriptionConfig } = config;
74
14
 
75
15
  return {
76
16
  name: 'subscription',
@@ -79,32 +19,16 @@ export function createSubscriptionInitModule(
79
19
  init: async () => {
80
20
  try {
81
21
  const apiKey = getApiKey();
82
-
83
22
  if (!apiKey) {
84
- if (typeof __DEV__ !== 'undefined' && __DEV__) {
85
- console.log('[createSubscriptionInitModule] No API key - skipping');
86
- }
87
- return true; // Not an error, just skip
88
- }
89
-
90
- const testStoreKey = getTestStoreKey?.();
91
-
92
- await initializeSubscription({
93
- apiKey,
94
- testStoreKey,
95
- ...subscriptionConfig,
96
- });
97
-
98
- if (typeof __DEV__ !== 'undefined' && __DEV__) {
99
- console.log('[createSubscriptionInitModule] Subscription initialized');
23
+ if (__DEV__) console.log('[SubscriptionInit] No API key - skipping');
24
+ return true;
100
25
  }
101
26
 
27
+ await initializeSubscription({ apiKey, ...subscriptionConfig });
28
+ if (__DEV__) console.log('[SubscriptionInit] Initialized');
102
29
  return true;
103
30
  } catch (error) {
104
- if (typeof __DEV__ !== 'undefined' && __DEV__) {
105
- console.error('[createSubscriptionInitModule] Error:', error);
106
- }
107
- // Continue on error - subscription is not critical for app launch
31
+ if (__DEV__) console.error('[SubscriptionInit] Error:', error);
108
32
  return true;
109
33
  }
110
34
  },
@@ -1,50 +1,34 @@
1
- /**
2
- * RevenueCat Configuration Value Object
3
- * Validates and stores RevenueCat configuration
4
- */
5
-
6
1
  import type { CustomerInfo } from "react-native-purchases";
7
2
 
8
3
  export interface RevenueCatConfig {
9
- /** Primary API key - resolved by main app based on platform */
10
4
  apiKey?: string;
11
- /** Test Store key for development/Expo Go testing */
12
- testStoreKey?: string;
13
- /** Entitlement identifier to check for premium status (REQUIRED - app specific) */
14
5
  entitlementIdentifier: string;
15
- /** Product identifiers for consumable products (e.g., credits packages) */
16
6
  consumableProductIdentifiers?: string[];
17
- /** Callback for premium status sync to database */
18
7
  onPremiumStatusChanged?: (
19
8
  userId: string,
20
9
  isPremium: boolean,
21
10
  productId?: string,
22
11
  expiresAt?: string,
23
12
  willRenew?: boolean,
24
- /** RevenueCat period type: NORMAL, INTRO, or TRIAL */
25
13
  periodType?: "NORMAL" | "INTRO" | "TRIAL"
26
14
  ) => Promise<void> | void;
27
- /** Callback for purchase completion */
28
15
  onPurchaseCompleted?: (
29
16
  userId: string,
30
17
  productId: string,
31
18
  customerInfo: CustomerInfo,
32
19
  source?: string
33
20
  ) => Promise<void> | void;
34
- /** Callback for restore completion */
35
21
  onRestoreCompleted?: (
36
22
  userId: string,
37
23
  isPremium: boolean,
38
24
  customerInfo: CustomerInfo
39
25
  ) => Promise<void> | void;
40
- /** Callback when subscription renewal is detected */
41
26
  onRenewalDetected?: (
42
27
  userId: string,
43
28
  productId: string,
44
29
  newExpirationDate: string,
45
30
  customerInfo: CustomerInfo
46
31
  ) => Promise<void> | void;
47
- /** Callback when subscription plan changes (upgrade/downgrade) */
48
32
  onPlanChanged?: (
49
33
  userId: string,
50
34
  newProductId: string,
@@ -52,11 +36,5 @@ export interface RevenueCatConfig {
52
36
  isUpgrade: boolean,
53
37
  customerInfo: CustomerInfo
54
38
  ) => Promise<void> | void;
55
- /** Callback after credits are successfully updated (for cache invalidation) */
56
39
  onCreditsUpdated?: (userId: string) => void;
57
40
  }
58
-
59
- export interface RevenueCatConfigRequired {
60
- apiKey: string;
61
- }
62
-
@@ -1,26 +1,15 @@
1
- /**
2
- * Offerings Fetcher
3
- * Handles RevenueCat offerings retrieval
4
- */
5
-
6
1
  import Purchases, { type PurchasesOffering } from "react-native-purchases";
7
2
 
8
3
  export interface OfferingsFetcherDeps {
9
- isInitialized: () => boolean;
10
- isUsingTestStore: () => boolean;
4
+ isInitialized: () => boolean;
11
5
  }
12
6
 
13
- export async function fetchOfferings(
14
- deps: OfferingsFetcherDeps
15
- ): Promise<PurchasesOffering | null> {
16
- if (!deps.isInitialized()) {
17
- return null;
18
- }
19
-
20
- try {
21
- const offerings = await Purchases.getOfferings();
22
- return offerings.current;
23
- } catch {
24
- return null;
25
- }
7
+ export async function fetchOfferings(deps: OfferingsFetcherDeps): Promise<PurchasesOffering | null> {
8
+ if (!deps.isInitialized()) return null;
9
+ try {
10
+ const offerings = await Purchases.getOfferings();
11
+ return offerings.current;
12
+ } catch {
13
+ return null;
14
+ }
26
15
  }
@@ -1,183 +1,66 @@
1
- /**
2
- * Purchase Handler
3
- * Handles RevenueCat purchase operations for both subscriptions and consumables
4
- */
5
-
6
1
  import Purchases, { type PurchasesPackage } from "react-native-purchases";
7
2
  import type { PurchaseResult } from "../../application/ports/IRevenueCatService";
8
- import {
9
- RevenueCatPurchaseError,
10
- RevenueCatInitializationError,
11
- } from "../../domain/errors/RevenueCatError";
3
+ import { RevenueCatPurchaseError, RevenueCatInitializationError } from "../../domain/errors/RevenueCatError";
12
4
  import type { RevenueCatConfig } from "../../domain/value-objects/RevenueCatConfig";
13
- import {
14
- isUserCancelledError,
15
- getErrorMessage,
16
- } from "../../domain/types/RevenueCatTypes";
17
- import {
18
- syncPremiumStatus,
19
- notifyPurchaseCompleted,
20
- } from "../utils/PremiumStatusSyncer";
5
+ import { isUserCancelledError, getErrorMessage } from "../../domain/types/RevenueCatTypes";
6
+ import { syncPremiumStatus, notifyPurchaseCompleted } from "../utils/PremiumStatusSyncer";
21
7
  import { getSavedPurchase, clearSavedPurchase } from "../../../presentation/hooks/useAuthAwarePurchase";
22
8
 
9
+ declare const __DEV__: boolean;
10
+
23
11
  export interface PurchaseHandlerDeps {
24
- config: RevenueCatConfig;
25
- isInitialized: () => boolean;
26
- isUsingTestStore: () => boolean;
12
+ config: RevenueCatConfig;
13
+ isInitialized: () => boolean;
27
14
  }
28
15
 
29
- function isConsumableProduct(
30
- pkg: PurchasesPackage,
31
- consumableIds: string[]
32
- ): boolean {
33
- if (consumableIds.length === 0) return false;
34
- const identifier = pkg.product.identifier.toLowerCase();
35
- return consumableIds.some((id) => identifier.includes(id.toLowerCase()));
16
+ function isConsumableProduct(pkg: PurchasesPackage, consumableIds: string[]): boolean {
17
+ if (consumableIds.length === 0) return false;
18
+ const identifier = pkg.product.identifier.toLowerCase();
19
+ return consumableIds.some((id) => identifier.includes(id.toLowerCase()));
36
20
  }
37
21
 
38
- /**
39
- * Handle package purchase - supports both subscriptions and consumables
40
- */
41
- declare const __DEV__: boolean;
42
-
43
22
  export async function handlePurchase(
44
- deps: PurchaseHandlerDeps,
45
- pkg: PurchasesPackage,
46
- userId: string
23
+ deps: PurchaseHandlerDeps,
24
+ pkg: PurchasesPackage,
25
+ userId: string
47
26
  ): Promise<PurchaseResult> {
48
- if (__DEV__) {
49
- console.log('[DEBUG PurchaseHandler] handlePurchase called', {
50
- productId: pkg.product.identifier,
51
- userId,
52
- isInitialized: deps.isInitialized(),
53
- });
54
- }
55
-
56
- if (!deps.isInitialized()) {
57
- if (__DEV__) {
58
- console.log('[DEBUG PurchaseHandler] Not initialized, throwing error');
59
- }
60
- throw new RevenueCatInitializationError();
61
- }
62
-
63
- const consumableIds = deps.config.consumableProductIdentifiers || [];
64
- const isConsumable = isConsumableProduct(pkg, consumableIds);
65
- const entitlementIdentifier = deps.config.entitlementIdentifier;
66
-
67
- try {
68
- if (__DEV__) {
69
- console.log('[DEBUG PurchaseHandler] Calling Purchases.purchasePackage...', {
70
- productId: pkg.product.identifier,
71
- packageIdentifier: pkg.identifier,
72
- offeringIdentifier: pkg.offeringIdentifier,
73
- });
74
- }
75
-
76
- const { customerInfo } = await Purchases.purchasePackage(pkg);
27
+ if (!deps.isInitialized()) throw new RevenueCatInitializationError();
77
28
 
78
- if (__DEV__) {
79
- console.log('[DEBUG PurchaseHandler] Purchase completed', {
80
- productId: pkg.product.identifier,
81
- activeEntitlements: Object.keys(customerInfo.entitlements.active),
82
- });
83
- }
29
+ const consumableIds = deps.config.consumableProductIdentifiers || [];
30
+ const isConsumable = isConsumableProduct(pkg, consumableIds);
31
+ const entitlementIdentifier = deps.config.entitlementIdentifier;
84
32
 
85
- // Get purchase source from saved purchase
86
- const savedPurchase = getSavedPurchase();
87
- const source = savedPurchase?.source;
33
+ try {
34
+ if (__DEV__) console.log('[Purchase] Starting:', pkg.product.identifier);
88
35
 
89
- if (isConsumable) {
90
- if (__DEV__) {
91
- console.log('[DEBUG PurchaseHandler] Consumable purchase SUCCESS', { source });
92
- }
93
- await notifyPurchaseCompleted(
94
- deps.config,
95
- userId,
96
- pkg.product.identifier,
97
- customerInfo,
98
- source
99
- );
100
- // Clear pending purchase after successful purchase
101
- clearSavedPurchase();
102
- return {
103
- success: true,
104
- isPremium: false,
105
- customerInfo,
106
- isConsumable: true,
107
- productId: pkg.product.identifier,
108
- };
109
- }
36
+ const { customerInfo } = await Purchases.purchasePackage(pkg);
37
+ const savedPurchase = getSavedPurchase();
38
+ const source = savedPurchase?.source;
110
39
 
111
- const isPremium = !!customerInfo.entitlements.active[entitlementIdentifier];
112
-
113
- if (__DEV__) {
114
- console.log('[DEBUG PurchaseHandler] Checking premium status', {
115
- entitlementIdentifier,
116
- isPremium,
117
- allEntitlements: customerInfo.entitlements.active,
118
- source,
119
- });
120
- }
40
+ if (isConsumable) {
41
+ await notifyPurchaseCompleted(deps.config, userId, pkg.product.identifier, customerInfo, source);
42
+ clearSavedPurchase();
43
+ return { success: true, isPremium: false, customerInfo, isConsumable: true, productId: pkg.product.identifier };
44
+ }
121
45
 
122
- if (isPremium) {
123
- if (__DEV__) {
124
- console.log('[DEBUG PurchaseHandler] Premium purchase SUCCESS', { source });
125
- }
126
- await syncPremiumStatus(deps.config, userId, customerInfo);
127
- await notifyPurchaseCompleted(
128
- deps.config,
129
- userId,
130
- pkg.product.identifier,
131
- customerInfo,
132
- source
133
- );
134
- // Clear pending purchase after successful purchase
135
- clearSavedPurchase();
136
- return { success: true, isPremium: true, customerInfo };
137
- }
46
+ const isPremium = !!customerInfo.entitlements.active[entitlementIdentifier];
138
47
 
139
- // In Preview API mode (Expo Go), purchases complete but entitlements aren't active
140
- // Treat the purchase as successful for testing purposes
141
- if (deps.isUsingTestStore()) {
142
- if (__DEV__) {
143
- console.log('[DEBUG PurchaseHandler] Test store purchase SUCCESS', { source });
144
- }
145
- await notifyPurchaseCompleted(
146
- deps.config,
147
- userId,
148
- pkg.product.identifier,
149
- customerInfo,
150
- source
151
- );
152
- // Clear pending purchase after successful purchase
153
- clearSavedPurchase();
154
- return { success: true, isPremium: false, customerInfo };
155
- }
48
+ if (isPremium) {
49
+ await syncPremiumStatus(deps.config, userId, customerInfo);
50
+ await notifyPurchaseCompleted(deps.config, userId, pkg.product.identifier, customerInfo, source);
51
+ clearSavedPurchase();
52
+ return { success: true, isPremium: true, customerInfo };
53
+ }
156
54
 
157
- if (__DEV__) {
158
- console.log('[DEBUG PurchaseHandler] Purchase FAILED - no entitlement');
159
- }
160
- throw new RevenueCatPurchaseError(
161
- "Purchase completed but premium entitlement not active",
162
- pkg.product.identifier
163
- );
164
- } catch (error) {
165
- if (__DEV__) {
166
- console.error('[DEBUG PurchaseHandler] Purchase error caught', {
167
- error,
168
- isUserCancelled: isUserCancelledError(error),
169
- });
170
- }
171
- if (isUserCancelledError(error)) {
172
- if (__DEV__) {
173
- console.log('[DEBUG PurchaseHandler] User cancelled');
174
- }
175
- return { success: false, isPremium: false };
176
- }
177
- const errorMessage = getErrorMessage(error, "Purchase failed");
178
- if (__DEV__) {
179
- console.error('[DEBUG PurchaseHandler] Throwing error:', errorMessage);
180
- }
181
- throw new RevenueCatPurchaseError(errorMessage, pkg.product.identifier);
55
+ // Purchase completed but no entitlement - still notify (test store scenario)
56
+ await notifyPurchaseCompleted(deps.config, userId, pkg.product.identifier, customerInfo, source);
57
+ clearSavedPurchase();
58
+ return { success: true, isPremium: false, customerInfo };
59
+ } catch (error) {
60
+ if (isUserCancelledError(error)) {
61
+ return { success: false, isPremium: false };
182
62
  }
63
+ const errorMessage = getErrorMessage(error, "Purchase failed");
64
+ throw new RevenueCatPurchaseError(errorMessage, pkg.product.identifier);
65
+ }
183
66
  }
@@ -1,52 +1,29 @@
1
- /**
2
- * Restore Handler
3
- * Handles RevenueCat restore operations
4
- */
5
-
6
1
  import Purchases from "react-native-purchases";
7
2
  import type { RestoreResult } from "../../application/ports/IRevenueCatService";
8
- import {
9
- RevenueCatRestoreError,
10
- RevenueCatInitializationError,
11
- } from "../../domain/errors/RevenueCatError";
3
+ import { RevenueCatRestoreError, RevenueCatInitializationError } from "../../domain/errors/RevenueCatError";
12
4
  import type { RevenueCatConfig } from "../../domain/value-objects/RevenueCatConfig";
13
5
  import { getErrorMessage } from "../../domain/types/RevenueCatTypes";
14
- import {
15
- syncPremiumStatus,
16
- notifyRestoreCompleted,
17
- } from "../utils/PremiumStatusSyncer";
6
+ import { syncPremiumStatus, notifyRestoreCompleted } from "../utils/PremiumStatusSyncer";
18
7
 
19
8
  export interface RestoreHandlerDeps {
20
- config: RevenueCatConfig;
21
- isInitialized: () => boolean;
22
- isUsingTestStore: () => boolean;
9
+ config: RevenueCatConfig;
10
+ isInitialized: () => boolean;
23
11
  }
24
12
 
25
- /**
26
- * Handle restore purchases
27
- */
28
- export async function handleRestore(
29
- deps: RestoreHandlerDeps,
30
- userId: string
31
- ): Promise<RestoreResult> {
32
- if (!deps.isInitialized()) {
33
- throw new RevenueCatInitializationError();
34
- }
35
-
36
- try {
37
- const customerInfo = await Purchases.restorePurchases();
38
- const entitlementIdentifier = deps.config.entitlementIdentifier;
39
- const isPremium = !!customerInfo.entitlements.active[entitlementIdentifier];
13
+ export async function handleRestore(deps: RestoreHandlerDeps, userId: string): Promise<RestoreResult> {
14
+ if (!deps.isInitialized()) throw new RevenueCatInitializationError();
40
15
 
41
- if (isPremium) {
42
- await syncPremiumStatus(deps.config, userId, customerInfo);
43
- }
16
+ try {
17
+ const customerInfo = await Purchases.restorePurchases();
18
+ const isPremium = !!customerInfo.entitlements.active[deps.config.entitlementIdentifier];
44
19
 
45
- await notifyRestoreCompleted(deps.config, userId, isPremium, customerInfo);
46
-
47
- return { success: isPremium, isPremium, customerInfo };
48
- } catch (error) {
49
- const errorMessage = getErrorMessage(error, "Restore failed");
50
- throw new RevenueCatRestoreError(errorMessage);
20
+ if (isPremium) {
21
+ await syncPremiumStatus(deps.config, userId, customerInfo);
51
22
  }
23
+ await notifyRestoreCompleted(deps.config, userId, isPremium, customerInfo);
24
+
25
+ return { success: isPremium, isPremium, customerInfo };
26
+ } catch (error) {
27
+ throw new RevenueCatRestoreError(getErrorMessage(error, "Restore failed"));
28
+ }
52
29
  }
@@ -1,186 +1,110 @@
1
- /**
2
- * RevenueCat Initializer
3
- * Handles SDK initialization logic
4
- */
5
-
6
1
  import Purchases, { LOG_LEVEL } from "react-native-purchases";
7
2
  import type { InitializeResult } from "../../application/ports/IRevenueCatService";
8
3
  import type { RevenueCatConfig } from "../../domain/value-objects/RevenueCatConfig";
9
- import { getErrorMessage } from "../../domain/types/RevenueCatTypes";
10
4
  import { resolveApiKey } from "../utils/ApiKeyResolver";
11
5
 
6
+ declare const __DEV__: boolean;
7
+
12
8
  export interface InitializerDeps {
13
- config: RevenueCatConfig;
14
- isUsingTestStore: () => boolean;
15
- isInitialized: () => boolean;
16
- getCurrentUserId: () => string | null;
17
- setInitialized: (value: boolean) => void;
18
- setCurrentUserId: (userId: string) => void;
9
+ config: RevenueCatConfig;
10
+ isInitialized: () => boolean;
11
+ getCurrentUserId: () => string | null;
12
+ setInitialized: (value: boolean) => void;
13
+ setCurrentUserId: (userId: string) => void;
19
14
  }
20
15
 
21
16
  let isPurchasesConfigured = false;
22
17
  let isLogHandlerConfigured = false;
23
- // Mutex to prevent concurrent configuration
24
18
  let configurationInProgress = false;
25
19
 
26
20
  function configureLogHandler(): void {
27
- if (isLogHandlerConfigured) return;
28
-
29
- Purchases.setLogHandler((logLevel, message) => {
30
- const isAppTransactionError =
31
- message.includes("Purchase was cancelled") ||
32
- message.includes("AppTransaction") ||
33
- message.includes("Couldn't find previous transactions");
34
-
35
- if (isAppTransactionError) {
36
- return;
37
- }
38
-
39
- switch (logLevel) {
40
- case LOG_LEVEL.ERROR:
41
- break;
42
- default:
43
- break;
44
- }
45
- });
46
-
47
- isLogHandlerConfigured = true;
21
+ if (isLogHandlerConfigured) return;
22
+ Purchases.setLogHandler((logLevel, message) => {
23
+ const ignoreMessages = ['Purchase was cancelled', 'AppTransaction', "Couldn't find previous transactions"];
24
+ if (ignoreMessages.some(m => message.includes(m))) return;
25
+ if (logLevel === LOG_LEVEL.ERROR && __DEV__) console.error('[RevenueCat]', message);
26
+ });
27
+ isLogHandlerConfigured = true;
48
28
  }
49
29
 
50
- function buildSuccessResult(
51
- deps: InitializerDeps,
52
- customerInfo: any,
53
- offerings: any
54
- ): InitializeResult {
55
- const entitlementId = deps.config.entitlementIdentifier;
56
- const hasPremium = !!customerInfo.entitlements.active[entitlementId];
57
- return { success: true, offering: offerings.current, hasPremium };
30
+ function buildSuccessResult(deps: InitializerDeps, customerInfo: any, offerings: any): InitializeResult {
31
+ const hasPremium = !!customerInfo.entitlements.active[deps.config.entitlementIdentifier];
32
+ return { success: true, offering: offerings.current, hasPremium };
58
33
  }
59
34
 
60
- declare const __DEV__: boolean;
61
-
62
35
  export async function initializeSDK(
63
- deps: InitializerDeps,
64
- userId: string,
65
- apiKey?: string
36
+ deps: InitializerDeps,
37
+ userId: string,
38
+ apiKey?: string
66
39
  ): Promise<InitializeResult> {
67
- if (__DEV__) {
68
- console.log('[DEBUG RevenueCatInitializer] initializeSDK called', {
69
- userId,
70
- hasApiKey: !!apiKey,
71
- isInitialized: deps.isInitialized(),
72
- currentUserId: deps.getCurrentUserId(),
73
- isPurchasesConfigured,
74
- });
75
- }
76
- // Case 1: Already initialized with the same user ID
77
- if (deps.isInitialized() && deps.getCurrentUserId() === userId) {
78
- try {
79
- const [customerInfo, offerings] = await Promise.all([
80
- Purchases.getCustomerInfo(),
81
- Purchases.getOfferings(),
82
- ]);
83
- return buildSuccessResult(deps, customerInfo, offerings);
84
- } catch {
85
- return { success: false, offering: null, hasPremium: false };
86
- }
87
- }
88
-
89
- // Case 2: Already configured but different user or re-initializing
90
- if (isPurchasesConfigured) {
91
- try {
92
- const currentAppUserId = await Purchases.getAppUserID();
93
-
94
- let customerInfo;
95
- if (currentAppUserId !== userId) {
96
- const result = await Purchases.logIn(userId);
97
- customerInfo = result.customerInfo;
98
- } else {
99
- customerInfo = await Purchases.getCustomerInfo();
100
- }
101
-
102
- deps.setInitialized(true);
103
- deps.setCurrentUserId(userId);
104
-
105
- const offerings = await Purchases.getOfferings();
106
- return buildSuccessResult(deps, customerInfo, offerings);
107
- } catch {
108
- return { success: false, offering: null, hasPremium: false };
109
- }
110
- }
111
-
112
- // Case 3: First time configuration
113
- // Check mutex to prevent double configuration
114
- if (configurationInProgress) {
115
- // Wait a bit and retry - another thread is configuring
116
- await new Promise(resolve => setTimeout(resolve, 100));
117
- // After waiting, isPurchasesConfigured should be true
118
- if (isPurchasesConfigured) {
119
- return initializeSDK(deps, userId, apiKey);
120
- }
121
- return { success: false, offering: null, hasPremium: false };
40
+ if (deps.isInitialized() && deps.getCurrentUserId() === userId) {
41
+ try {
42
+ const [customerInfo, offerings] = await Promise.all([
43
+ Purchases.getCustomerInfo(),
44
+ Purchases.getOfferings(),
45
+ ]);
46
+ return buildSuccessResult(deps, customerInfo, offerings);
47
+ } catch {
48
+ return { success: false, offering: null, hasPremium: false };
122
49
  }
50
+ }
123
51
 
124
- if (__DEV__) {
125
- console.log('[DEBUG RevenueCatInitializer] Config received:', {
126
- hasApiKey: !!deps.config.apiKey,
127
- hasTestStoreKey: !!deps.config.testStoreKey,
128
- apiKeyPrefix: deps.config.apiKey?.substring(0, 10),
129
- testStoreKeyPrefix: deps.config.testStoreKey?.substring(0, 10),
130
- });
52
+ if (isPurchasesConfigured) {
53
+ try {
54
+ const currentAppUserId = await Purchases.getAppUserID();
55
+ let customerInfo;
56
+ if (currentAppUserId !== userId) {
57
+ const result = await Purchases.logIn(userId);
58
+ customerInfo = result.customerInfo;
59
+ } else {
60
+ customerInfo = await Purchases.getCustomerInfo();
61
+ }
62
+ deps.setInitialized(true);
63
+ deps.setCurrentUserId(userId);
64
+ const offerings = await Purchases.getOfferings();
65
+ return buildSuccessResult(deps, customerInfo, offerings);
66
+ } catch {
67
+ return { success: false, offering: null, hasPremium: false };
131
68
  }
132
-
133
- const key = apiKey || resolveApiKey(deps.config);
69
+ }
70
+
71
+ if (configurationInProgress) {
72
+ await new Promise(resolve => setTimeout(resolve, 100));
73
+ if (isPurchasesConfigured) return initializeSDK(deps, userId, apiKey);
74
+ return { success: false, offering: null, hasPremium: false };
75
+ }
76
+
77
+ const key = apiKey || resolveApiKey(deps.config);
78
+ if (!key) {
79
+ if (__DEV__) console.log('[RevenueCat] No API key');
80
+ return { success: false, offering: null, hasPremium: false };
81
+ }
82
+
83
+ configurationInProgress = true;
84
+ try {
85
+ configureLogHandler();
86
+ if (__DEV__) console.log('[RevenueCat] Configuring:', key.substring(0, 10) + '...');
87
+
88
+ await Purchases.configure({ apiKey: key, appUserID: userId });
89
+ isPurchasesConfigured = true;
90
+ deps.setInitialized(true);
91
+ deps.setCurrentUserId(userId);
92
+
93
+ const [customerInfo, offerings] = await Promise.all([
94
+ Purchases.getCustomerInfo(),
95
+ Purchases.getOfferings(),
96
+ ]);
134
97
 
135
98
  if (__DEV__) {
136
- console.log('[DEBUG RevenueCatInitializer] Resolved key:', key ? key.substring(0, 10) + '...' : 'null');
137
- }
138
-
139
- if (!key) {
140
- if (__DEV__) {
141
- console.log('[DEBUG RevenueCatInitializer] No API key available, returning failure');
142
- }
143
- return { success: false, offering: null, hasPremium: false };
144
- }
145
-
146
- // Acquire mutex
147
- configurationInProgress = true;
148
-
149
- try {
150
- configureLogHandler();
151
-
152
- if (__DEV__) {
153
- console.log('[DEBUG RevenueCatInitializer] Configuring Purchases SDK with userId:', userId);
154
- }
155
- await Purchases.configure({
156
- apiKey: key,
157
- appUserID: userId,
158
- });
159
- isPurchasesConfigured = true;
160
- deps.setInitialized(true);
161
- deps.setCurrentUserId(userId);
162
-
163
- if (__DEV__) {
164
- console.log('[DEBUG RevenueCatInitializer] Purchases configured, fetching customer info and offerings...');
165
- }
166
- const [customerInfo, offerings] = await Promise.all([
167
- Purchases.getCustomerInfo(),
168
- Purchases.getOfferings(),
169
- ]);
170
-
171
- if (__DEV__) {
172
- console.log('[DEBUG RevenueCatInitializer] Init complete', {
173
- hasOfferings: !!offerings.current,
174
- offeringsIdentifier: offerings.current?.identifier,
175
- packagesCount: offerings.current?.availablePackages?.length ?? 0,
176
- });
177
- }
178
- return buildSuccessResult(deps, customerInfo, offerings);
179
- } catch (error) {
180
- getErrorMessage(error, "RevenueCat init failed");
181
- return { success: false, offering: null, hasPremium: false };
182
- } finally {
183
- // Release mutex
184
- configurationInProgress = false;
99
+ console.log('[RevenueCat] Initialized', {
100
+ packages: offerings.current?.availablePackages?.length ?? 0,
101
+ });
185
102
  }
103
+ return buildSuccessResult(deps, customerInfo, offerings);
104
+ } catch (error) {
105
+ if (__DEV__) console.error('[RevenueCat] Init failed:', error);
106
+ return { success: false, offering: null, hasPremium: false };
107
+ } finally {
108
+ configurationInProgress = false;
109
+ }
186
110
  }
@@ -41,10 +41,6 @@ export class RevenueCatService implements IRevenueCatService {
41
41
  return this.stateManager.isInitialized();
42
42
  }
43
43
 
44
- isUsingTestStore(): boolean {
45
- return this.stateManager.isUsingTestStore();
46
- }
47
-
48
44
  getCurrentUserId(): string | null {
49
45
  return this.stateManager.getCurrentUserId();
50
46
  }
@@ -52,7 +48,6 @@ export class RevenueCatService implements IRevenueCatService {
52
48
  private getSDKParams() {
53
49
  return {
54
50
  config: this.stateManager.getConfig(),
55
- isUsingTestStore: () => this.isUsingTestStore(),
56
51
  isInitialized: () => this.isInitialized(),
57
52
  getCurrentUserId: () => this.stateManager.getCurrentUserId(),
58
53
  setInitialized: (value: boolean) => this.stateManager.setInitialized(value),
@@ -1,32 +1,17 @@
1
- /**
2
- * Service State Manager
3
- * Manages RevenueCat service state
4
- */
5
-
6
1
  import type { RevenueCatConfig } from '../../domain/value-objects/RevenueCatConfig';
7
- import { isExpoGo, isDevelopment } from '../utils/ExpoGoDetector';
8
2
 
9
3
  export class ServiceStateManager {
10
- private isInitializedFlag: boolean = false;
11
- private usingTestStore: boolean = false;
4
+ private isInitializedFlag = false;
12
5
  private currentUserId: string | null = null;
13
6
  private config: RevenueCatConfig;
14
7
 
15
8
  constructor(config: RevenueCatConfig) {
16
9
  this.config = config;
17
- this.usingTestStore = this.shouldUseTestStore();
18
- }
19
-
20
- private shouldUseTestStore(): boolean {
21
- const testKey = this.config.testStoreKey;
22
- return !!(testKey && (isExpoGo() || isDevelopment()));
23
10
  }
24
11
 
25
12
  setInitialized(value: boolean): void {
26
13
  this.isInitializedFlag = value;
27
- if (!value) {
28
- this.currentUserId = null;
29
- }
14
+ if (!value) this.currentUserId = null;
30
15
  }
31
16
 
32
17
  isInitialized(): boolean {
@@ -41,16 +26,7 @@ export class ServiceStateManager {
41
26
  return this.currentUserId;
42
27
  }
43
28
 
44
- isUsingTestStore(): boolean {
45
- return this.usingTestStore;
46
- }
47
-
48
29
  getConfig(): RevenueCatConfig {
49
30
  return this.config;
50
31
  }
51
-
52
- updateConfig(config: RevenueCatConfig): void {
53
- this.config = config;
54
- this.usingTestStore = this.shouldUseTestStore();
55
- }
56
32
  }
@@ -1,65 +1,5 @@
1
- /**
2
- * API Key Resolver
3
- * Resolves RevenueCat API key from configuration
4
- * NOTE: Main app is responsible for resolving platform-specific keys
5
- */
6
-
7
1
  import type { RevenueCatConfig } from '../../domain/value-objects/RevenueCatConfig';
8
- import { isTestStoreEnvironment } from "./ExpoGoDetector";
9
-
10
- declare const __DEV__: boolean;
11
-
12
- /**
13
- * Check if Test Store key should be used
14
- * CRITICAL: Never use test store in production builds
15
- * Uses Test Store in development environments (Expo Go, dev builds, simulators)
16
- */
17
- export function shouldUseTestStore(config: RevenueCatConfig): boolean {
18
- const testKey = config.testStoreKey;
19
-
20
- if (!testKey) {
21
- return false;
22
- }
23
-
24
- return isTestStoreEnvironment();
25
- }
26
2
 
27
- /**
28
- * Get RevenueCat API key from config
29
- * Returns Test Store key in development environments (Expo Go, dev builds, simulators)
30
- * Returns production API key in production builds
31
- * Main app must provide resolved platform-specific apiKey in config
32
- */
33
3
  export function resolveApiKey(config: RevenueCatConfig): string | null {
34
- const useTestStore = shouldUseTestStore(config);
35
-
36
- if (__DEV__) {
37
- console.log('[DEBUG resolveApiKey] called', {
38
- useTestStore,
39
- hasTestStoreKey: !!config.testStoreKey,
40
- hasApiKey: !!config.apiKey,
41
- apiKeyPrefix: config.apiKey?.substring(0, 10),
42
- });
43
- }
44
-
45
- if (useTestStore) {
46
- if (__DEV__) {
47
- console.log('[DEBUG resolveApiKey] Using test store key');
48
- }
49
- return config.testStoreKey ?? null;
50
- }
51
-
52
- const key = config.apiKey;
53
-
54
- if (!key || key === "" || key.includes("YOUR_")) {
55
- if (__DEV__) {
56
- console.log('[DEBUG resolveApiKey] No valid API key found');
57
- }
58
- return null;
59
- }
60
-
61
- if (__DEV__) {
62
- console.log('[DEBUG resolveApiKey] Using production API key:', key.substring(0, 10) + '...');
63
- }
64
- return key;
4
+ return config.apiKey || null;
65
5
  }
@@ -1,58 +0,0 @@
1
- /**
2
- * Expo Go Detector
3
- * Detects runtime environment for RevenueCat configuration
4
- */
5
-
6
- import Constants from "expo-constants";
7
-
8
- /**
9
- * Check if running in Expo Go
10
- */
11
- export function isExpoGo(): boolean {
12
- return Constants.executionEnvironment === "storeClient";
13
- }
14
-
15
- /**
16
- * Check if running in development mode
17
- * Uses multiple checks to ensure reliability in production builds
18
- */
19
- export function isDevelopment(): boolean {
20
- // Check execution environment first - most reliable
21
- const executionEnv = Constants.executionEnvironment;
22
- const isBareBuild = executionEnv === "bare";
23
- const isStoreBuild = executionEnv === "standalone";
24
-
25
- // If it's a store/standalone build, it's NOT development
26
- if (isStoreBuild) {
27
- return false;
28
- }
29
-
30
- // For bare builds in production, check appOwnership
31
- if (isBareBuild && Constants.appOwnership !== "expo") {
32
- // This is a production bare build
33
- return false;
34
- }
35
-
36
- // Fallback to __DEV__ only for actual development cases
37
- return typeof __DEV__ !== "undefined" && __DEV__;
38
- }
39
-
40
- /**
41
- * Check if this is a production store build
42
- */
43
- export function isProductionBuild(): boolean {
44
- const executionEnv = Constants.executionEnvironment;
45
- return executionEnv === "standalone" || executionEnv === "bare";
46
- }
47
-
48
- /**
49
- * Check if Test Store should be used (Expo Go or development)
50
- * NEVER use Test Store in production builds
51
- */
52
- export function isTestStoreEnvironment(): boolean {
53
- // Explicit check: never use test store in production
54
- if (isProductionBuild() && !isExpoGo()) {
55
- return false;
56
- }
57
- return isExpoGo() || isDevelopment();
58
- }