@umituz/react-native-subscription 2.27.34 → 2.27.36
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 -34
- 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 +93 -155
- 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 -61
- 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.36",
|
|
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,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 {
|
|
48
|
+
const {
|
|
49
|
+
apiKey, apiKeyIos, apiKeyAndroid, entitlementId, credits,
|
|
50
|
+
getAnonymousUserId, getFirebaseAuth, showAuthModal,
|
|
51
|
+
onCreditsUpdated, creditPackages, timeoutMs = 10000, authStateTimeoutMs = 2000,
|
|
52
|
+
} = config;
|
|
40
53
|
|
|
41
|
-
|
|
42
|
-
|
|
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 ||
|
|
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
|
-
|
|
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,186 +1,124 @@
|
|
|
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
|
-
|
|
21
|
+
if (isLogHandlerConfigured) return;
|
|
22
|
+
if (typeof Purchases.setLogHandler !== 'function') return;
|
|
23
|
+
try {
|
|
29
24
|
Purchases.setLogHandler((logLevel, message) => {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
}
|
|
25
|
+
const ignoreMessages = ['Purchase was cancelled', 'AppTransaction', "Couldn't find previous transactions"];
|
|
26
|
+
if (ignoreMessages.some(m => message.includes(m))) return;
|
|
27
|
+
if (logLevel === LOG_LEVEL.ERROR && __DEV__) console.error('[RevenueCat]', message);
|
|
45
28
|
});
|
|
46
|
-
|
|
47
29
|
isLogHandlerConfigured = true;
|
|
30
|
+
} catch {
|
|
31
|
+
// Native module not available (Expo Go)
|
|
32
|
+
}
|
|
48
33
|
}
|
|
49
34
|
|
|
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 };
|
|
35
|
+
function buildSuccessResult(deps: InitializerDeps, customerInfo: any, offerings: any): InitializeResult {
|
|
36
|
+
const hasPremium = !!customerInfo.entitlements.active[deps.config.entitlementIdentifier];
|
|
37
|
+
return { success: true, offering: offerings.current, hasPremium };
|
|
58
38
|
}
|
|
59
39
|
|
|
60
|
-
|
|
40
|
+
function isNativeModuleAvailable(): boolean {
|
|
41
|
+
return typeof Purchases?.configure === 'function';
|
|
42
|
+
}
|
|
61
43
|
|
|
62
44
|
export async function initializeSDK(
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
45
|
+
deps: InitializerDeps,
|
|
46
|
+
userId: string,
|
|
47
|
+
apiKey?: string
|
|
66
48
|
): Promise<InitializeResult> {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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);
|
|
49
|
+
if (!isNativeModuleAvailable()) {
|
|
50
|
+
if (__DEV__) console.log('[RevenueCat] Native module not available (Expo Go)');
|
|
51
|
+
return { success: false, offering: null, hasPremium: false };
|
|
52
|
+
}
|
|
104
53
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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 };
|
|
54
|
+
if (deps.isInitialized() && deps.getCurrentUserId() === userId) {
|
|
55
|
+
try {
|
|
56
|
+
const [customerInfo, offerings] = await Promise.all([
|
|
57
|
+
Purchases.getCustomerInfo(),
|
|
58
|
+
Purchases.getOfferings(),
|
|
59
|
+
]);
|
|
60
|
+
return buildSuccessResult(deps, customerInfo, offerings);
|
|
61
|
+
} catch {
|
|
62
|
+
return { success: false, offering: null, hasPremium: false };
|
|
122
63
|
}
|
|
64
|
+
}
|
|
123
65
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
66
|
+
if (isPurchasesConfigured) {
|
|
67
|
+
try {
|
|
68
|
+
const currentAppUserId = await Purchases.getAppUserID();
|
|
69
|
+
let customerInfo;
|
|
70
|
+
if (currentAppUserId !== userId) {
|
|
71
|
+
const result = await Purchases.logIn(userId);
|
|
72
|
+
customerInfo = result.customerInfo;
|
|
73
|
+
} else {
|
|
74
|
+
customerInfo = await Purchases.getCustomerInfo();
|
|
75
|
+
}
|
|
76
|
+
deps.setInitialized(true);
|
|
77
|
+
deps.setCurrentUserId(userId);
|
|
78
|
+
const offerings = await Purchases.getOfferings();
|
|
79
|
+
return buildSuccessResult(deps, customerInfo, offerings);
|
|
80
|
+
} catch {
|
|
81
|
+
return { success: false, offering: null, hasPremium: false };
|
|
131
82
|
}
|
|
132
|
-
|
|
133
|
-
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (configurationInProgress) {
|
|
86
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
87
|
+
if (isPurchasesConfigured) return initializeSDK(deps, userId, apiKey);
|
|
88
|
+
return { success: false, offering: null, hasPremium: false };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const key = apiKey || resolveApiKey(deps.config);
|
|
92
|
+
if (!key) {
|
|
93
|
+
if (__DEV__) console.log('[RevenueCat] No API key');
|
|
94
|
+
return { success: false, offering: null, hasPremium: false };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
configurationInProgress = true;
|
|
98
|
+
try {
|
|
99
|
+
configureLogHandler();
|
|
100
|
+
if (__DEV__) console.log('[RevenueCat] Configuring:', key.substring(0, 10) + '...');
|
|
101
|
+
|
|
102
|
+
await Purchases.configure({ apiKey: key, appUserID: userId });
|
|
103
|
+
isPurchasesConfigured = true;
|
|
104
|
+
deps.setInitialized(true);
|
|
105
|
+
deps.setCurrentUserId(userId);
|
|
106
|
+
|
|
107
|
+
const [customerInfo, offerings] = await Promise.all([
|
|
108
|
+
Purchases.getCustomerInfo(),
|
|
109
|
+
Purchases.getOfferings(),
|
|
110
|
+
]);
|
|
134
111
|
|
|
135
112
|
if (__DEV__) {
|
|
136
|
-
|
|
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;
|
|
113
|
+
console.log('[RevenueCat] Initialized', {
|
|
114
|
+
packages: offerings.current?.availablePackages?.length ?? 0,
|
|
115
|
+
});
|
|
185
116
|
}
|
|
117
|
+
return buildSuccessResult(deps, customerInfo, offerings);
|
|
118
|
+
} catch (error) {
|
|
119
|
+
if (__DEV__) console.error('[RevenueCat] Init failed:', error);
|
|
120
|
+
return { success: false, offering: null, hasPremium: false };
|
|
121
|
+
} finally {
|
|
122
|
+
configurationInProgress = false;
|
|
123
|
+
}
|
|
186
124
|
}
|
|
@@ -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,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
|
-
|
|
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
|
-
}
|