@umituz/react-native-subscription 2.27.92 → 2.27.94
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/domains/credits/application/CreditsInitializer.ts +99 -40
- package/src/domains/credits/application/DeductCreditsCommand.ts +31 -13
- package/src/domains/credits/application/PurchaseMetadataGenerator.ts +17 -23
- package/src/domains/credits/core/Credits.ts +39 -39
- package/src/domains/credits/core/CreditsMapper.ts +11 -10
- package/src/domains/credits/core/UserCreditsDocument.ts +33 -33
- package/src/domains/credits/infrastructure/CreditsRepository.ts +46 -59
- package/src/domains/paywall/components/PaywallModal.tsx +1 -1
- package/src/domains/subscription/application/SubscriptionInitializer.ts +59 -18
- package/src/domains/subscription/application/SubscriptionInitializerTypes.ts +20 -20
- package/src/domains/subscription/infrastructure/handlers/PackageHandler.ts +46 -27
- package/src/domains/subscription/infrastructure/managers/SubscriptionManager.ts +106 -42
- package/src/domains/subscription/infrastructure/services/RestoreHandler.ts +4 -2
- package/src/domains/subscription/infrastructure/services/RevenueCatInitializer.ts +1 -2
- package/src/domains/subscription/infrastructure/utils/RenewalDetector.ts +1 -1
- package/src/domains/subscription/presentation/components/details/PremiumStatusBadge.tsx +6 -4
- package/src/domains/subscription/presentation/components/feedback/PaywallFeedbackModal.tsx +1 -1
- package/src/domains/subscription/presentation/types/SubscriptionDetailTypes.ts +4 -2
- package/src/domains/subscription/presentation/types/SubscriptionSettingsTypes.ts +1 -1
- package/src/domains/subscription/presentation/usePremiumGate.ts +1 -1
- package/src/domains/subscription/presentation/useSavedPurchaseAutoExecution.ts +1 -1
- package/src/domains/subscription/presentation/useSubscriptionSettingsConfig.ts +4 -3
- package/src/domains/subscription/presentation/useSubscriptionSettingsConfig.utils.ts +1 -1
- package/src/domains/trial/application/TrialEligibilityService.ts +1 -1
- package/src/domains/trial/infrastructure/DeviceTrialRepository.ts +2 -2
- package/src/shared/application/ports/IRevenueCatService.ts +2 -0
- package/src/shared/infrastructure/SubscriptionEventBus.ts +5 -2
- package/src/presentation/README.md +0 -125
- package/src/presentation/hooks/README.md +0 -156
- package/src/presentation/hooks/useAuthSubscriptionSync.md +0 -94
- package/src/presentation/hooks/useCredits.md +0 -103
- package/src/presentation/hooks/useDeductCredit.md +0 -100
- package/src/presentation/hooks/useFeatureGate.md +0 -112
- package/src/presentation/hooks/usePaywall.md +0 -89
- package/src/presentation/hooks/usePaywallOperations.md +0 -92
- package/src/presentation/hooks/usePaywallVisibility.md +0 -95
- package/src/presentation/hooks/usePremium.md +0 -88
- package/src/presentation/hooks/useSubscriptionSettingsConfig.md +0 -94
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Optimized to use Design Patterns: Command, Observer, and Strategy.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { doc, getDoc,
|
|
6
|
+
import { doc, getDoc, type Firestore } from "firebase/firestore";
|
|
7
7
|
import { BaseRepository, getFirestore } from "@umituz/react-native-firebase";
|
|
8
8
|
import type { CreditsConfig, CreditsResult, DeductCreditsResult } from "../core/Credits";
|
|
9
9
|
import type { UserCreditsDocumentRead, PurchaseSource } from "../core/UserCreditsDocument";
|
|
@@ -16,8 +16,8 @@ import { CreditLimitCalculator } from "../application/CreditLimitCalculator";
|
|
|
16
16
|
export class CreditsRepository extends BaseRepository {
|
|
17
17
|
private deductCommand: DeductCreditsCommand;
|
|
18
18
|
|
|
19
|
-
constructor(private config: CreditsConfig) {
|
|
20
|
-
super();
|
|
19
|
+
constructor(private config: CreditsConfig) {
|
|
20
|
+
super();
|
|
21
21
|
this.deductCommand = new DeductCreditsCommand((db, uid) => this.getRef(db, uid));
|
|
22
22
|
}
|
|
23
23
|
|
|
@@ -29,74 +29,61 @@ export class CreditsRepository extends BaseRepository {
|
|
|
29
29
|
|
|
30
30
|
async getCredits(userId: string): Promise<CreditsResult> {
|
|
31
31
|
const db = getFirestore();
|
|
32
|
-
if (!db)
|
|
33
|
-
|
|
34
|
-
const snap = await getDoc(this.getRef(db, userId));
|
|
35
|
-
if (!snap.exists()) return { success: true, data: undefined };
|
|
36
|
-
|
|
37
|
-
const entity = CreditsMapper.toEntity(snap.data() as UserCreditsDocumentRead);
|
|
38
|
-
return { success: true, data: entity };
|
|
39
|
-
} catch (e: unknown) {
|
|
40
|
-
const message = e instanceof Error ? e.message : String(e);
|
|
41
|
-
return { success: false, error: { message, code: "FETCH_ERR" } };
|
|
32
|
+
if (!db) {
|
|
33
|
+
throw new Error("Firestore instance is not available");
|
|
42
34
|
}
|
|
35
|
+
|
|
36
|
+
const snap = await getDoc(this.getRef(db, userId));
|
|
37
|
+
if (!snap.exists()) {
|
|
38
|
+
return { success: true, data: null, error: null };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const entity = CreditsMapper.toEntity(snap.data() as UserCreditsDocumentRead);
|
|
42
|
+
return { success: true, data: entity, error: null };
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
async initializeCredits(
|
|
46
|
-
userId: string,
|
|
47
|
-
|
|
46
|
+
userId: string,
|
|
47
|
+
purchaseId: string,
|
|
48
|
+
productId: string,
|
|
49
|
+
source: PurchaseSource,
|
|
50
|
+
revenueCatData: RevenueCatData
|
|
48
51
|
): Promise<CreditsResult> {
|
|
49
52
|
const db = getFirestore();
|
|
50
|
-
if (!db)
|
|
51
|
-
|
|
52
|
-
// Use CreditLimitCalculator (Refactoring Logic)
|
|
53
|
-
const creditLimit = CreditLimitCalculator.calculate(productId, this.config);
|
|
54
|
-
const cfg = { ...this.config, creditLimit };
|
|
55
|
-
|
|
56
|
-
const result = await initializeCreditsTransaction(db, this.getRef(db, userId), cfg, purchaseId, {
|
|
57
|
-
productId, source,
|
|
58
|
-
expirationDate: revenueCatData?.expirationDate,
|
|
59
|
-
willRenew: revenueCatData?.willRenew,
|
|
60
|
-
originalTransactionId: revenueCatData?.originalTransactionId,
|
|
61
|
-
isPremium: revenueCatData?.isPremium,
|
|
62
|
-
periodType: revenueCatData?.periodType,
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
return {
|
|
66
|
-
success: true,
|
|
67
|
-
data: result.finalData ? CreditsMapper.toEntity(result.finalData) : undefined,
|
|
68
|
-
};
|
|
69
|
-
} catch (e: unknown) {
|
|
70
|
-
const message = e instanceof Error ? e.message : String(e);
|
|
71
|
-
return { success: false, error: { message, code: "INIT_ERR" } };
|
|
53
|
+
if (!db) {
|
|
54
|
+
throw new Error("Firestore instance is not available");
|
|
72
55
|
}
|
|
56
|
+
|
|
57
|
+
const creditLimit = CreditLimitCalculator.calculate(productId, this.config);
|
|
58
|
+
const cfg = { ...this.config, creditLimit };
|
|
59
|
+
|
|
60
|
+
const result = await initializeCreditsTransaction(
|
|
61
|
+
db,
|
|
62
|
+
this.getRef(db, userId),
|
|
63
|
+
cfg,
|
|
64
|
+
purchaseId,
|
|
65
|
+
{
|
|
66
|
+
productId,
|
|
67
|
+
source,
|
|
68
|
+
expirationDate: revenueCatData.expirationDate,
|
|
69
|
+
willRenew: revenueCatData.willRenew,
|
|
70
|
+
originalTransactionId: revenueCatData.originalTransactionId,
|
|
71
|
+
isPremium: revenueCatData.isPremium,
|
|
72
|
+
periodType: revenueCatData.periodType,
|
|
73
|
+
}
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
success: true,
|
|
78
|
+
data: result.finalData ? CreditsMapper.toEntity(result.finalData) : null,
|
|
79
|
+
error: null,
|
|
80
|
+
};
|
|
73
81
|
}
|
|
74
82
|
|
|
75
83
|
/**
|
|
76
84
|
* Delegates to DeductCreditsCommand (Command Pattern)
|
|
77
85
|
*/
|
|
78
|
-
async deductCredit(userId: string, cost: number
|
|
86
|
+
async deductCredit(userId: string, cost: number): Promise<DeductCreditsResult> {
|
|
79
87
|
return this.deductCommand.execute(userId, cost);
|
|
80
88
|
}
|
|
81
|
-
|
|
82
|
-
async hasCredits(userId: string, cost: number = 1): Promise<boolean> {
|
|
83
|
-
const res = await this.getCredits(userId);
|
|
84
|
-
return !!(res.success && res.data && res.data.credits >= cost);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
async syncExpiredStatus(userId: string): Promise<void> {
|
|
88
|
-
const db = getFirestore();
|
|
89
|
-
if (!db) return;
|
|
90
|
-
try {
|
|
91
|
-
await updateDoc(this.getRef(db, userId), {
|
|
92
|
-
isPremium: false,
|
|
93
|
-
status: "expired",
|
|
94
|
-
lastUpdatedAt: serverTimestamp()
|
|
95
|
-
});
|
|
96
|
-
} catch (e) {
|
|
97
|
-
if (__DEV__) console.error("[CreditsRepository] Sync expired failed:", e);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
89
|
}
|
|
101
|
-
|
|
102
|
-
export const createCreditsRepository = (c: CreditsConfig) => new CreditsRepository(c);
|
|
@@ -12,7 +12,7 @@ import type { SubscriptionFeature, PaywallTranslations, PaywallLegalUrls } from
|
|
|
12
12
|
import { paywallModalStyles as styles } from "./PaywallModal.styles";
|
|
13
13
|
import { PaywallFeatures } from "./PaywallFeatures";
|
|
14
14
|
import { PaywallFooter } from "./PaywallFooter";
|
|
15
|
-
import { usePurchaseLoadingStore, selectIsPurchasing } from "
|
|
15
|
+
import { usePurchaseLoadingStore, selectIsPurchasing } from "../../subscription/presentation/stores";
|
|
16
16
|
|
|
17
17
|
/** Trial eligibility info per product */
|
|
18
18
|
export interface TrialEligibilityInfo {
|
|
@@ -18,16 +18,47 @@ export type { FirebaseAuthLike, CreditPackageConfig, SubscriptionInitConfig } fr
|
|
|
18
18
|
|
|
19
19
|
export const initializeSubscription = async (config: SubscriptionInitConfig): Promise<void> => {
|
|
20
20
|
const {
|
|
21
|
-
apiKey,
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
apiKey,
|
|
22
|
+
apiKeyIos,
|
|
23
|
+
apiKeyAndroid,
|
|
24
|
+
entitlementId,
|
|
25
|
+
credits,
|
|
26
|
+
getAnonymousUserId,
|
|
27
|
+
getFirebaseAuth,
|
|
28
|
+
showAuthModal,
|
|
29
|
+
onCreditsUpdated,
|
|
30
|
+
creditPackages,
|
|
24
31
|
} = config;
|
|
25
32
|
|
|
26
|
-
const key = Platform.OS === 'ios'
|
|
27
|
-
|
|
33
|
+
const key = Platform.OS === 'ios'
|
|
34
|
+
? (apiKeyIos || apiKey)
|
|
35
|
+
: (apiKeyAndroid || apiKey);
|
|
36
|
+
|
|
37
|
+
if (!key) {
|
|
38
|
+
throw new Error('API key required');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (!creditPackages) {
|
|
42
|
+
throw new Error('creditPackages is required');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (!creditPackages.identifierPattern) {
|
|
46
|
+
throw new Error('creditPackages.identifierPattern is required');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (!creditPackages.amounts) {
|
|
50
|
+
throw new Error('creditPackages.amounts is required');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (!getAnonymousUserId) {
|
|
54
|
+
throw new Error('getAnonymousUserId is required');
|
|
55
|
+
}
|
|
28
56
|
|
|
29
57
|
// 1. Configure Repository
|
|
30
|
-
configureCreditsRepository({
|
|
58
|
+
configureCreditsRepository({
|
|
59
|
+
...credits,
|
|
60
|
+
creditPackageAmounts: creditPackages.amounts
|
|
61
|
+
});
|
|
31
62
|
|
|
32
63
|
// 2. Setup Sync Service
|
|
33
64
|
const syncService = new SubscriptionSyncService(entitlementId);
|
|
@@ -37,7 +68,7 @@ export const initializeSubscription = async (config: SubscriptionInitConfig): Pr
|
|
|
37
68
|
config: {
|
|
38
69
|
apiKey: key,
|
|
39
70
|
entitlementIdentifier: entitlementId,
|
|
40
|
-
consumableProductIdentifiers: [creditPackages
|
|
71
|
+
consumableProductIdentifiers: [creditPackages.identifierPattern],
|
|
41
72
|
onPurchaseCompleted: (u: string, p: string, c: any, s: any) => syncService.handlePurchase(u, p, c, s),
|
|
42
73
|
onRenewalDetected: (u: string, p: string, expires: string, c: any) => syncService.handleRenewal(u, p, expires, c),
|
|
43
74
|
onPremiumStatusChanged: (u: string, isP: boolean, pId: any, exp: any, willR: any, pt: any) => syncService.handlePremiumStatusChanged(u, isP, pId, exp, willR, pt),
|
|
@@ -50,28 +81,38 @@ export const initializeSubscription = async (config: SubscriptionInitConfig): Pr
|
|
|
50
81
|
// 4. Configure Auth aware actions
|
|
51
82
|
configureAuthProvider({
|
|
52
83
|
isAuthenticated: () => {
|
|
53
|
-
const
|
|
84
|
+
const auth = getFirebaseAuth();
|
|
85
|
+
if (!auth) {
|
|
86
|
+
throw new Error("Firebase auth is not available");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const u = auth.currentUser;
|
|
54
90
|
return !!(u && !u.isAnonymous);
|
|
55
91
|
},
|
|
56
92
|
showAuthModal,
|
|
57
93
|
});
|
|
58
94
|
|
|
59
|
-
const initializeInBackground = async (userId
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
} catch (error) {
|
|
64
|
-
if (__DEV__) console.log('[SubscriptionInitializer] Background init failed (non-critical):', error);
|
|
95
|
+
const initializeInBackground = async (userId: string): Promise<void> => {
|
|
96
|
+
await SubscriptionManager.initialize(userId);
|
|
97
|
+
if (__DEV__) {
|
|
98
|
+
console.log('[SubscriptionInitializer] Background init complete');
|
|
65
99
|
}
|
|
66
100
|
};
|
|
67
101
|
|
|
68
102
|
// 5. Start Background Init
|
|
69
|
-
const
|
|
70
|
-
|
|
103
|
+
const auth = getFirebaseAuth();
|
|
104
|
+
if (!auth) {
|
|
105
|
+
throw new Error("Firebase auth is not available");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const initialUserId = getCurrentUserId(() => auth);
|
|
109
|
+
await initializeInBackground(initialUserId);
|
|
71
110
|
|
|
72
111
|
// 6. Listen for Auth Changes
|
|
73
|
-
setupAuthStateListener(
|
|
74
|
-
if (__DEV__)
|
|
112
|
+
setupAuthStateListener(() => auth, (newUserId) => {
|
|
113
|
+
if (__DEV__) {
|
|
114
|
+
console.log('[SubscriptionInitializer] Auth changed, re-init:', newUserId);
|
|
115
|
+
}
|
|
75
116
|
initializeInBackground(newUserId);
|
|
76
117
|
});
|
|
77
118
|
};
|
|
@@ -13,38 +13,38 @@ export interface FirebaseAuthLike {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
export interface CreditPackageConfig {
|
|
16
|
-
identifierPattern
|
|
17
|
-
amounts
|
|
16
|
+
identifierPattern: string;
|
|
17
|
+
amounts: Record<string, number>;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
export interface SubscriptionInitConfig {
|
|
21
|
-
apiKey
|
|
22
|
-
apiKeyIos
|
|
23
|
-
apiKeyAndroid
|
|
21
|
+
apiKey: string;
|
|
22
|
+
apiKeyIos: string;
|
|
23
|
+
apiKeyAndroid: string;
|
|
24
24
|
entitlementId: string;
|
|
25
25
|
credits: CreditsConfig;
|
|
26
26
|
getAnonymousUserId: () => Promise<string>;
|
|
27
27
|
getFirebaseAuth: () => FirebaseAuthLike | null;
|
|
28
28
|
showAuthModal: () => void;
|
|
29
|
-
onCreditsUpdated
|
|
30
|
-
creditPackages
|
|
31
|
-
timeoutMs
|
|
32
|
-
authStateTimeoutMs
|
|
29
|
+
onCreditsUpdated: (userId: string) => void;
|
|
30
|
+
creditPackages: CreditPackageConfig;
|
|
31
|
+
timeoutMs: number;
|
|
32
|
+
authStateTimeoutMs: number;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
export interface InitializeCreditsMetadata {
|
|
36
|
-
productId
|
|
37
|
-
source
|
|
38
|
-
type
|
|
39
|
-
expirationDate
|
|
40
|
-
willRenew
|
|
41
|
-
originalTransactionId
|
|
42
|
-
isPremium
|
|
43
|
-
periodType
|
|
36
|
+
productId: string;
|
|
37
|
+
source: PurchaseSource;
|
|
38
|
+
type: PurchaseType;
|
|
39
|
+
expirationDate: string | null;
|
|
40
|
+
willRenew: boolean | null;
|
|
41
|
+
originalTransactionId: string | null;
|
|
42
|
+
isPremium: boolean;
|
|
43
|
+
periodType: PeriodType | null;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
export interface InitializationResult {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
47
|
+
credits: number;
|
|
48
|
+
alreadyProcessed: boolean;
|
|
49
|
+
finalData: UserCreditsDocumentRead | null;
|
|
50
50
|
}
|
|
@@ -15,48 +15,67 @@ export interface RestoreResultInfo {
|
|
|
15
15
|
|
|
16
16
|
export class PackageHandler {
|
|
17
17
|
constructor(
|
|
18
|
-
private service: IRevenueCatService
|
|
18
|
+
private service: IRevenueCatService,
|
|
19
19
|
private entitlementId: string
|
|
20
20
|
) { }
|
|
21
21
|
|
|
22
|
-
setService
|
|
22
|
+
setService(service: IRevenueCatService): void {
|
|
23
|
+
this.service = service;
|
|
24
|
+
}
|
|
23
25
|
|
|
24
26
|
async fetchPackages(): Promise<PurchasesPackage[]> {
|
|
25
|
-
if (!this.service
|
|
26
|
-
|
|
27
|
-
const offering = await this.service.fetchOfferings();
|
|
28
|
-
return offering?.availablePackages ?? [];
|
|
29
|
-
} catch (error) {
|
|
30
|
-
if (__DEV__) console.error('[PackageHandler] fetchOfferings failed:', error);
|
|
31
|
-
return [];
|
|
27
|
+
if (!this.service.isInitialized()) {
|
|
28
|
+
throw new Error("Service not initialized");
|
|
32
29
|
}
|
|
30
|
+
|
|
31
|
+
const offering = await this.service.fetchOfferings();
|
|
32
|
+
|
|
33
|
+
if (!offering) {
|
|
34
|
+
throw new Error("No offerings available");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const packages = offering.availablePackages;
|
|
38
|
+
if (!packages) {
|
|
39
|
+
throw new Error("No packages available in offering");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return packages;
|
|
33
43
|
}
|
|
34
44
|
|
|
35
45
|
async purchase(pkg: PurchasesPackage, userId: string): Promise<boolean> {
|
|
36
|
-
if (!this.service
|
|
37
|
-
|
|
38
|
-
const result = await this.service.purchasePackage(pkg, userId);
|
|
39
|
-
return result.success;
|
|
40
|
-
} catch (error) {
|
|
41
|
-
if (__DEV__) console.error('[PackageHandler] Purchase failed:', error);
|
|
42
|
-
return false;
|
|
46
|
+
if (!this.service.isInitialized()) {
|
|
47
|
+
throw new Error("Service not initialized");
|
|
43
48
|
}
|
|
49
|
+
|
|
50
|
+
const result = await this.service.purchasePackage(pkg, userId);
|
|
51
|
+
return result.success;
|
|
44
52
|
}
|
|
45
53
|
|
|
46
54
|
async restore(userId: string): Promise<RestoreResultInfo> {
|
|
47
|
-
if (!this.service
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}
|
|
55
|
-
return { success: result.success, productId };
|
|
56
|
-
} catch (error) {
|
|
57
|
-
if (__DEV__) console.error('[PackageHandler] Restore failed:', error);
|
|
55
|
+
if (!this.service.isInitialized()) {
|
|
56
|
+
throw new Error("Service not initialized");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const result = await this.service.restorePurchases(userId);
|
|
60
|
+
|
|
61
|
+
if (!result.success) {
|
|
58
62
|
return { success: false, productId: null };
|
|
59
63
|
}
|
|
64
|
+
|
|
65
|
+
if (!result.customerInfo) {
|
|
66
|
+
return { success: true, productId: null };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const entitlement = getPremiumEntitlement(result.customerInfo, this.entitlementId);
|
|
70
|
+
|
|
71
|
+
if (!entitlement) {
|
|
72
|
+
return { success: true, productId: null };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
success: true,
|
|
77
|
+
productId: entitlement.productIdentifier,
|
|
78
|
+
};
|
|
60
79
|
}
|
|
61
80
|
|
|
62
81
|
checkPremiumStatusFromInfo(customerInfo: CustomerInfo): PremiumStatus {
|
|
@@ -14,7 +14,7 @@ import { SubscriptionInternalState } from "./SubscriptionInternalState";
|
|
|
14
14
|
export interface SubscriptionManagerConfig {
|
|
15
15
|
config: RevenueCatConfig;
|
|
16
16
|
apiKey: string;
|
|
17
|
-
getAnonymousUserId
|
|
17
|
+
getAnonymousUserId: () => Promise<string>;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
class SubscriptionManagerImpl {
|
|
@@ -25,97 +25,161 @@ class SubscriptionManagerImpl {
|
|
|
25
25
|
|
|
26
26
|
configure(config: SubscriptionManagerConfig): void {
|
|
27
27
|
this.managerConfig = config;
|
|
28
|
-
this.
|
|
29
|
-
|
|
28
|
+
this.state.userIdProvider.configure(config.getAnonymousUserId);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
private ensurePackageHandlerInitialized(): void {
|
|
32
|
+
if (this.packageHandler) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (!this.serviceInstance) {
|
|
37
|
+
throw new Error("Service instance not available");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (!this.managerConfig) {
|
|
41
|
+
throw new Error("Manager not configured");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
this.packageHandler = new PackageHandler(
|
|
45
|
+
this.serviceInstance,
|
|
46
|
+
this.managerConfig.config.entitlementIdentifier
|
|
47
|
+
);
|
|
30
48
|
}
|
|
31
49
|
|
|
32
50
|
private ensureConfigured(): void {
|
|
33
|
-
if (!this.managerConfig
|
|
51
|
+
if (!this.managerConfig) {
|
|
52
|
+
throw new Error("SubscriptionManager not configured");
|
|
53
|
+
}
|
|
34
54
|
}
|
|
35
55
|
|
|
36
|
-
async initialize(userId
|
|
56
|
+
async initialize(userId: string): Promise<boolean> {
|
|
37
57
|
this.ensureConfigured();
|
|
38
|
-
const effectiveUserId = userId || (await this.state.userIdProvider.getOrCreateAnonymousUserId());
|
|
39
|
-
const { shouldInit, existingPromise } = this.state.initCache.tryAcquireInitialization(effectiveUserId);
|
|
40
58
|
|
|
41
|
-
|
|
59
|
+
const { shouldInit, existingPromise } = this.state.initCache.tryAcquireInitialization(userId);
|
|
60
|
+
|
|
61
|
+
if (!shouldInit && existingPromise) {
|
|
62
|
+
return existingPromise;
|
|
63
|
+
}
|
|
42
64
|
|
|
43
65
|
const promise = (async () => {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
console.error('[SubscriptionManager] Service instance not available after initialization');
|
|
50
|
-
}
|
|
51
|
-
return false;
|
|
52
|
-
}
|
|
53
|
-
this.packageHandler!.setService(this.serviceInstance);
|
|
54
|
-
const result = await this.serviceInstance.initialize(effectiveUserId);
|
|
55
|
-
return result.success;
|
|
56
|
-
} catch (error) {
|
|
57
|
-
if (__DEV__) {
|
|
58
|
-
console.error('[SubscriptionManager] Initialization failed:', error);
|
|
59
|
-
}
|
|
60
|
-
return false;
|
|
66
|
+
await initializeRevenueCatService(this.managerConfig!.config);
|
|
67
|
+
this.serviceInstance = getRevenueCatService();
|
|
68
|
+
|
|
69
|
+
if (!this.serviceInstance) {
|
|
70
|
+
throw new Error("Service instance not available after initialization");
|
|
61
71
|
}
|
|
72
|
+
|
|
73
|
+
this.ensurePackageHandlerInitialized();
|
|
74
|
+
const result = await this.serviceInstance.initialize(userId);
|
|
75
|
+
return result.success;
|
|
62
76
|
})();
|
|
63
77
|
|
|
64
|
-
this.state.initCache.setPromise(promise,
|
|
78
|
+
this.state.initCache.setPromise(promise, userId);
|
|
65
79
|
return promise;
|
|
66
80
|
}
|
|
67
81
|
|
|
68
82
|
isInitializedForUser(userId: string): boolean {
|
|
69
|
-
|
|
83
|
+
if (!this.serviceInstance) {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (!this.serviceInstance.isInitialized()) {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return this.state.initCache.getCurrentUserId() === userId;
|
|
70
92
|
}
|
|
71
93
|
|
|
72
94
|
async getPackages(): Promise<PurchasesPackage[]> {
|
|
73
95
|
this.ensureConfigured();
|
|
96
|
+
|
|
74
97
|
if (!this.serviceInstance) {
|
|
75
98
|
this.serviceInstance = getRevenueCatService();
|
|
76
|
-
this.packageHandler!.setService(this.serviceInstance);
|
|
77
99
|
}
|
|
100
|
+
|
|
101
|
+
if (!this.serviceInstance) {
|
|
102
|
+
throw new Error("Service instance not available");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
this.ensurePackageHandlerInitialized();
|
|
78
106
|
return this.packageHandler!.fetchPackages();
|
|
79
107
|
}
|
|
80
108
|
|
|
81
109
|
async purchasePackage(pkg: PurchasesPackage): Promise<boolean> {
|
|
82
110
|
this.ensureConfigured();
|
|
111
|
+
|
|
83
112
|
const userId = this.state.initCache.getCurrentUserId();
|
|
84
|
-
if (!userId)
|
|
113
|
+
if (!userId) {
|
|
114
|
+
throw new Error("No current user found");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
this.ensurePackageHandlerInitialized();
|
|
85
118
|
return this.packageHandler!.purchase(pkg, userId);
|
|
86
119
|
}
|
|
87
120
|
|
|
88
121
|
async restore(): Promise<RestoreResultInfo> {
|
|
89
122
|
this.ensureConfigured();
|
|
123
|
+
|
|
90
124
|
const userId = this.state.initCache.getCurrentUserId();
|
|
91
|
-
if (!userId)
|
|
125
|
+
if (!userId) {
|
|
126
|
+
throw new Error("No current user found");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
this.ensurePackageHandlerInitialized();
|
|
92
130
|
return this.packageHandler!.restore(userId);
|
|
93
131
|
}
|
|
94
132
|
|
|
95
133
|
async checkPremiumStatus(): Promise<PremiumStatus> {
|
|
96
134
|
this.ensureConfigured();
|
|
135
|
+
|
|
97
136
|
const userId = this.state.initCache.getCurrentUserId();
|
|
98
|
-
if (!userId)
|
|
137
|
+
if (!userId) {
|
|
138
|
+
throw new Error("No current user found");
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (!this.serviceInstance) {
|
|
142
|
+
throw new Error("Service instance not available");
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const customerInfo = await this.serviceInstance.getCustomerInfo();
|
|
99
146
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
if (customerInfo) return this.packageHandler!.checkPremiumStatusFromInfo(customerInfo);
|
|
103
|
-
} catch (error) {
|
|
104
|
-
throw error;
|
|
147
|
+
if (!customerInfo) {
|
|
148
|
+
throw new Error("Customer info not available");
|
|
105
149
|
}
|
|
106
|
-
|
|
150
|
+
|
|
151
|
+
this.ensurePackageHandlerInitialized();
|
|
152
|
+
return this.packageHandler!.checkPremiumStatusFromInfo(customerInfo);
|
|
107
153
|
}
|
|
108
154
|
|
|
109
155
|
async reset(): Promise<void> {
|
|
110
|
-
if (this.serviceInstance)
|
|
156
|
+
if (this.serviceInstance) {
|
|
157
|
+
await this.serviceInstance.reset();
|
|
158
|
+
}
|
|
159
|
+
|
|
111
160
|
this.state.reset();
|
|
112
161
|
this.serviceInstance = null;
|
|
113
162
|
}
|
|
114
163
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
164
|
+
isConfigured(): boolean {
|
|
165
|
+
return this.managerConfig !== null;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
isInitialized(): boolean {
|
|
169
|
+
if (!this.serviceInstance) {
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return this.serviceInstance.isInitialized();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
getEntitlementId(): string {
|
|
177
|
+
if (!this.managerConfig) {
|
|
178
|
+
throw new Error("SubscriptionManager not configured");
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return this.managerConfig.config.entitlementIdentifier;
|
|
182
|
+
}
|
|
119
183
|
}
|
|
120
184
|
|
|
121
185
|
export const SubscriptionManager = new SubscriptionManagerImpl();
|
|
@@ -15,14 +15,16 @@ export async function handleRestore(deps: RestoreHandlerDeps, userId: string): P
|
|
|
15
15
|
|
|
16
16
|
try {
|
|
17
17
|
const customerInfo = await Purchases.restorePurchases();
|
|
18
|
-
const
|
|
18
|
+
const entitlement = customerInfo.entitlements.active[deps.config.entitlementIdentifier];
|
|
19
|
+
const isPremium = !!entitlement;
|
|
20
|
+
const productId = entitlement?.productIdentifier ?? null;
|
|
19
21
|
|
|
20
22
|
if (isPremium) {
|
|
21
23
|
await syncPremiumStatus(deps.config, userId, customerInfo);
|
|
22
24
|
}
|
|
23
25
|
await notifyRestoreCompleted(deps.config, userId, isPremium, customerInfo);
|
|
24
26
|
|
|
25
|
-
return { success: true, isPremium, customerInfo };
|
|
27
|
+
return { success: true, isPremium, productId, customerInfo };
|
|
26
28
|
} catch (error) {
|
|
27
29
|
throw new RevenueCatRestoreError(getErrorMessage(error, "Restore failed"));
|
|
28
30
|
}
|
|
@@ -19,8 +19,7 @@ const configurationState = {
|
|
|
19
19
|
configurationPromise: null as Promise<ReturnType<typeof initializeSDK>> | null,
|
|
20
20
|
};
|
|
21
21
|
|
|
22
|
-
// Simple lock mechanism to prevent concurrent configurations
|
|
23
|
-
let configurationLocks = new Set<string>();
|
|
22
|
+
// Simple lock mechanism to prevent concurrent configurations (implementation deferred)
|
|
24
23
|
|
|
25
24
|
function configureLogHandler(): void {
|
|
26
25
|
if (configurationState.isLogHandlerConfigured) return;
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type { CustomerInfo } from "react-native-purchases";
|
|
8
|
-
import { detectPackageType
|
|
8
|
+
import { detectPackageType } from "../../../../utils/packageTypeDetector";
|
|
9
9
|
|
|
10
10
|
export interface RenewalState {
|
|
11
11
|
previousExpirationDate: string | null;
|