@umituz/react-native-subscription 2.27.33 → 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 +1 -1
- package/src/infrastructure/services/SubscriptionInitializer.ts +20 -8
- package/src/init/createSubscriptionInitModule.ts +7 -83
- package/src/revenuecat/domain/value-objects/RevenueCatConfig.ts +0 -22
- package/src/revenuecat/infrastructure/services/OfferingsFetcher.ts +9 -20
- package/src/revenuecat/infrastructure/services/PurchaseHandler.ts +45 -162
- package/src/revenuecat/infrastructure/services/RestoreHandler.ts +17 -40
- package/src/revenuecat/infrastructure/services/RevenueCatInitializer.ts +85 -145
- package/src/revenuecat/infrastructure/services/RevenueCatService.ts +0 -5
- package/src/revenuecat/infrastructure/services/ServiceStateManager.ts +2 -26
- package/src/revenuecat/infrastructure/utils/ApiKeyResolver.ts +1 -41
- package/src/revenuecat/infrastructure/utils/ExpoGoDetector.ts +0 -58
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-subscription",
|
|
3
|
-
"version": "2.27.
|
|
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;
|
|
24
|
-
|
|
25
|
-
|
|
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,10 +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 {
|
|
48
|
+
const {
|
|
49
|
+
apiKey, apiKeyIos, apiKeyAndroid, entitlementId, credits,
|
|
50
|
+
getAnonymousUserId, getFirebaseAuth, showAuthModal,
|
|
51
|
+
onCreditsUpdated, creditPackages, timeoutMs = 10000, authStateTimeoutMs = 2000,
|
|
52
|
+
} = config;
|
|
40
53
|
|
|
41
|
-
const key = Platform.OS ===
|
|
42
|
-
if (!key) throw new Error(
|
|
54
|
+
const key = Platform.OS === 'ios' ? (apiKeyIos || apiKey || '') : (apiKeyAndroid || apiKey || '');
|
|
55
|
+
if (!key) throw new Error('API key required');
|
|
43
56
|
|
|
44
57
|
configureCreditsRepository({ ...credits, creditPackageAmounts: creditPackages?.amounts });
|
|
45
58
|
|
|
@@ -191,9 +204,8 @@ export const initializeSubscription = async (config: SubscriptionInitConfig): Pr
|
|
|
191
204
|
SubscriptionManager.configure({
|
|
192
205
|
config: {
|
|
193
206
|
apiKey: key,
|
|
194
|
-
testStoreKey,
|
|
195
207
|
entitlementIdentifier: entitlementId,
|
|
196
|
-
consumableProductIdentifiers: [creditPackages?.identifierPattern ||
|
|
208
|
+
consumableProductIdentifiers: [creditPackages?.identifierPattern || 'credit'],
|
|
197
209
|
onPurchaseCompleted: onPurchase,
|
|
198
210
|
onRenewalDetected: onRenewal,
|
|
199
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
|
-
|
|
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 (
|
|
85
|
-
|
|
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 (
|
|
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
|
-
|
|
10
|
-
isUsingTestStore: () => boolean;
|
|
4
|
+
isInitialized: () => boolean;
|
|
11
5
|
}
|
|
12
6
|
|
|
13
|
-
export async function fetchOfferings(
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
isUsingTestStore: () => boolean;
|
|
12
|
+
config: RevenueCatConfig;
|
|
13
|
+
isInitialized: () => boolean;
|
|
27
14
|
}
|
|
28
15
|
|
|
29
|
-
function isConsumableProduct(
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
)
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
23
|
+
deps: PurchaseHandlerDeps,
|
|
24
|
+
pkg: PurchasesPackage,
|
|
25
|
+
userId: string
|
|
47
26
|
): Promise<PurchaseResult> {
|
|
48
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
const source = savedPurchase?.source;
|
|
33
|
+
try {
|
|
34
|
+
if (__DEV__) console.log('[Purchase] Starting:', pkg.product.identifier);
|
|
88
35
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
isUsingTestStore: () => boolean;
|
|
9
|
+
config: RevenueCatConfig;
|
|
10
|
+
isInitialized: () => boolean;
|
|
23
11
|
}
|
|
24
12
|
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
16
|
+
try {
|
|
17
|
+
const customerInfo = await Purchases.restorePurchases();
|
|
18
|
+
const isPremium = !!customerInfo.entitlements.active[deps.config.entitlementIdentifier];
|
|
44
19
|
|
|
45
|
-
|
|
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,170 +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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
52
|
-
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
36
|
+
deps: InitializerDeps,
|
|
37
|
+
userId: string,
|
|
38
|
+
apiKey?: string
|
|
66
39
|
): Promise<InitializeResult> {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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 };
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
const key = apiKey || resolveApiKey(deps.config);
|
|
125
|
-
|
|
126
|
-
if (!key) {
|
|
127
|
-
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 };
|
|
128
49
|
}
|
|
50
|
+
}
|
|
129
51
|
|
|
130
|
-
|
|
131
|
-
configurationInProgress = true;
|
|
132
|
-
|
|
52
|
+
if (isPurchasesConfigured) {
|
|
133
53
|
try {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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 };
|
|
68
|
+
}
|
|
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
|
+
]);
|
|
154
97
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
packagesCount: offerings.current?.availablePackages?.length ?? 0,
|
|
160
|
-
});
|
|
161
|
-
}
|
|
162
|
-
return buildSuccessResult(deps, customerInfo, offerings);
|
|
163
|
-
} catch (error) {
|
|
164
|
-
getErrorMessage(error, "RevenueCat init failed");
|
|
165
|
-
return { success: false, offering: null, hasPremium: false };
|
|
166
|
-
} finally {
|
|
167
|
-
// Release mutex
|
|
168
|
-
configurationInProgress = false;
|
|
98
|
+
if (__DEV__) {
|
|
99
|
+
console.log('[RevenueCat] Initialized', {
|
|
100
|
+
packages: offerings.current?.availablePackages?.length ?? 0,
|
|
101
|
+
});
|
|
169
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
|
+
}
|
|
170
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
|
|
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,45 +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
|
-
/**
|
|
11
|
-
* Check if Test Store key should be used
|
|
12
|
-
* CRITICAL: Never use test store in production builds
|
|
13
|
-
* Uses Test Store in development environments (Expo Go, dev builds, simulators)
|
|
14
|
-
*/
|
|
15
|
-
export function shouldUseTestStore(config: RevenueCatConfig): boolean {
|
|
16
|
-
const testKey = config.testStoreKey;
|
|
17
|
-
|
|
18
|
-
if (!testKey) {
|
|
19
|
-
return false;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
return isTestStoreEnvironment();
|
|
23
|
-
}
|
|
24
2
|
|
|
25
|
-
/**
|
|
26
|
-
* Get RevenueCat API key from config
|
|
27
|
-
* Returns Test Store key in development environments (Expo Go, dev builds, simulators)
|
|
28
|
-
* Returns production API key in production builds
|
|
29
|
-
* Main app must provide resolved platform-specific apiKey in config
|
|
30
|
-
*/
|
|
31
3
|
export function resolveApiKey(config: RevenueCatConfig): string | null {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
if (useTestStore) {
|
|
35
|
-
return config.testStoreKey ?? null;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const key = config.apiKey;
|
|
39
|
-
|
|
40
|
-
if (!key || key === "" || key.includes("YOUR_")) {
|
|
41
|
-
return null;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
return key;
|
|
4
|
+
return config.apiKey || null;
|
|
45
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
|
-
}
|