@umituz/react-native-subscription 2.27.142 → 2.27.144
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/subscription/application/SubscriptionSyncProcessor.ts +24 -57
- package/src/domains/subscription/application/statusChangeHandlers.ts +54 -0
- package/src/domains/subscription/application/syncConstants.ts +11 -0
- package/src/domains/subscription/application/syncEventEmitter.ts +5 -0
- package/src/domains/subscription/application/syncIdGenerators.ts +19 -0
- package/src/domains/subscription/core/SubscriptionDisplayConfig.ts +0 -6
- package/src/domains/subscription/infrastructure/managers/SubscriptionManager.ts +24 -70
- package/src/domains/subscription/infrastructure/managers/initializationHandler.ts +14 -0
- package/src/domains/subscription/infrastructure/managers/managerConstants.ts +3 -0
- package/src/domains/subscription/infrastructure/managers/managerOperations.ts +38 -0
- package/src/domains/subscription/infrastructure/managers/packageHandlerFactory.ts +17 -0
- package/src/domains/subscription/infrastructure/managers/premiumStatusChecker.ts +17 -0
- package/src/domains/subscription/infrastructure/services/RevenueCatInitializer.ts +9 -35
- package/src/domains/subscription/infrastructure/services/RevenueCatInitializer.types.ts +9 -0
- package/src/domains/subscription/infrastructure/services/RevenueCatService.ts +2 -134
- package/src/domains/subscription/infrastructure/services/RevenueCatService.types.ts +87 -0
- package/src/domains/subscription/infrastructure/services/initializerConstants.ts +9 -0
- package/src/domains/subscription/infrastructure/services/revenueCatServiceInstance.ts +17 -0
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.144",
|
|
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",
|
|
@@ -1,23 +1,19 @@
|
|
|
1
1
|
import type { CustomerInfo } from "react-native-purchases";
|
|
2
|
-
import type {
|
|
3
|
-
import {
|
|
4
|
-
type PeriodType,
|
|
5
|
-
type PurchaseSource,
|
|
6
|
-
PURCHASE_SOURCE,
|
|
7
|
-
PURCHASE_TYPE
|
|
8
|
-
} from "../core/SubscriptionConstants";
|
|
2
|
+
import type { PeriodType, PurchaseSource } from "../core/SubscriptionConstants";
|
|
3
|
+
import { PURCHASE_SOURCE, PURCHASE_TYPE } from "../core/SubscriptionConstants";
|
|
9
4
|
import { getCreditsRepository } from "../../credits/infrastructure/CreditsRepositoryManager";
|
|
10
5
|
import { extractRevenueCatData } from "./SubscriptionSyncUtils";
|
|
11
|
-
import {
|
|
6
|
+
import { emitCreditsUpdated } from "./syncEventEmitter";
|
|
7
|
+
import { generatePurchaseId, generateRenewalId } from "./syncIdGenerators";
|
|
8
|
+
import { handleExpiredSubscription, handleFreeUserInitialization, handlePremiumStatusSync } from "./statusChangeHandlers";
|
|
9
|
+
import { NO_SUBSCRIPTION_PRODUCT_ID } from "./syncConstants";
|
|
12
10
|
|
|
13
11
|
export class SubscriptionSyncProcessor {
|
|
14
12
|
constructor(private entitlementId: string) {}
|
|
15
13
|
|
|
16
14
|
async processPurchase(userId: string, productId: string, customerInfo: CustomerInfo, source?: PurchaseSource) {
|
|
17
15
|
const revenueCatData = extractRevenueCatData(customerInfo, this.entitlementId);
|
|
18
|
-
const purchaseId = revenueCatData.originalTransactionId
|
|
19
|
-
? `purchase_${revenueCatData.originalTransactionId}`
|
|
20
|
-
: `purchase_${productId}_${Date.now()}`;
|
|
16
|
+
const purchaseId = generatePurchaseId(revenueCatData.originalTransactionId, productId);
|
|
21
17
|
|
|
22
18
|
await getCreditsRepository().initializeCredits(
|
|
23
19
|
userId,
|
|
@@ -28,15 +24,13 @@ export class SubscriptionSyncProcessor {
|
|
|
28
24
|
PURCHASE_TYPE.INITIAL
|
|
29
25
|
);
|
|
30
26
|
|
|
31
|
-
|
|
27
|
+
emitCreditsUpdated(userId);
|
|
32
28
|
}
|
|
33
29
|
|
|
34
30
|
async processRenewal(userId: string, productId: string, newExpirationDate: string, customerInfo: CustomerInfo) {
|
|
35
31
|
const revenueCatData = extractRevenueCatData(customerInfo, this.entitlementId);
|
|
36
32
|
revenueCatData.expirationDate = newExpirationDate || revenueCatData.expirationDate;
|
|
37
|
-
const purchaseId = revenueCatData.originalTransactionId
|
|
38
|
-
? `renewal_${revenueCatData.originalTransactionId}_${newExpirationDate}`
|
|
39
|
-
: `renewal_${productId}_${Date.now()}`;
|
|
33
|
+
const purchaseId = generateRenewalId(revenueCatData.originalTransactionId, productId, newExpirationDate);
|
|
40
34
|
|
|
41
35
|
await getCreditsRepository().initializeCredits(
|
|
42
36
|
userId,
|
|
@@ -47,19 +41,19 @@ export class SubscriptionSyncProcessor {
|
|
|
47
41
|
PURCHASE_TYPE.RENEWAL
|
|
48
42
|
);
|
|
49
43
|
|
|
50
|
-
|
|
44
|
+
emitCreditsUpdated(userId);
|
|
51
45
|
}
|
|
52
46
|
|
|
53
47
|
async processStatusChange(
|
|
54
|
-
userId: string,
|
|
55
|
-
isPremium: boolean,
|
|
48
|
+
userId: string,
|
|
49
|
+
isPremium: boolean,
|
|
56
50
|
productId?: string,
|
|
57
|
-
expiresAt?: string,
|
|
58
|
-
willRenew?: boolean,
|
|
51
|
+
expiresAt?: string,
|
|
52
|
+
willRenew?: boolean,
|
|
59
53
|
periodType?: PeriodType
|
|
60
54
|
) {
|
|
61
55
|
const repository = getCreditsRepository();
|
|
62
|
-
|
|
56
|
+
|
|
63
57
|
if (!isPremium && !productId) {
|
|
64
58
|
const currentCredits = await repository.getCredits(userId);
|
|
65
59
|
if (currentCredits.success && currentCredits.data?.isPremium) {
|
|
@@ -68,49 +62,22 @@ export class SubscriptionSyncProcessor {
|
|
|
68
62
|
}
|
|
69
63
|
|
|
70
64
|
if (!isPremium && productId) {
|
|
71
|
-
await
|
|
72
|
-
subscriptionEventBus.emit(SUBSCRIPTION_EVENTS.CREDITS_UPDATED, userId);
|
|
65
|
+
await handleExpiredSubscription(userId);
|
|
73
66
|
return;
|
|
74
67
|
}
|
|
75
68
|
|
|
76
69
|
if (!isPremium && !productId) {
|
|
77
|
-
|
|
78
|
-
await repository.initializeCredits(
|
|
79
|
-
userId,
|
|
80
|
-
stableSyncId,
|
|
81
|
-
'no_subscription',
|
|
82
|
-
PURCHASE_SOURCE.SETTINGS,
|
|
83
|
-
{
|
|
84
|
-
isPremium: false,
|
|
85
|
-
expirationDate: null,
|
|
86
|
-
willRenew: false,
|
|
87
|
-
periodType: null,
|
|
88
|
-
originalTransactionId: null
|
|
89
|
-
},
|
|
90
|
-
PURCHASE_TYPE.INITIAL
|
|
91
|
-
);
|
|
92
|
-
subscriptionEventBus.emit(SUBSCRIPTION_EVENTS.CREDITS_UPDATED, userId);
|
|
70
|
+
await handleFreeUserInitialization(userId);
|
|
93
71
|
return;
|
|
94
72
|
}
|
|
95
73
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
const statusSyncId = `status_sync_${userId}_${isPremium ? 'premium' : 'free'}`;
|
|
105
|
-
await repository.initializeCredits(
|
|
106
|
-
userId,
|
|
107
|
-
statusSyncId,
|
|
108
|
-
productId ?? 'no_subscription',
|
|
109
|
-
PURCHASE_SOURCE.SETTINGS,
|
|
110
|
-
revenueCatData,
|
|
111
|
-
PURCHASE_TYPE.INITIAL
|
|
74
|
+
await handlePremiumStatusSync(
|
|
75
|
+
userId,
|
|
76
|
+
isPremium,
|
|
77
|
+
productId ?? NO_SUBSCRIPTION_PRODUCT_ID,
|
|
78
|
+
expiresAt ?? null,
|
|
79
|
+
willRenew ?? false,
|
|
80
|
+
periodType ?? null
|
|
112
81
|
);
|
|
113
|
-
|
|
114
|
-
subscriptionEventBus.emit(SUBSCRIPTION_EVENTS.CREDITS_UPDATED, userId);
|
|
115
82
|
}
|
|
116
83
|
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { RevenueCatData } from "../core/RevenueCatData";
|
|
2
|
+
import type { PeriodType } from "../core/SubscriptionConstants";
|
|
3
|
+
import { PURCHASE_SOURCE, PURCHASE_TYPE } from "../core/SubscriptionConstants";
|
|
4
|
+
import { getCreditsRepository } from "../../credits/infrastructure/CreditsRepositoryManager";
|
|
5
|
+
import { emitCreditsUpdated } from "./syncEventEmitter";
|
|
6
|
+
import { generateInitSyncId, generateStatusSyncId } from "./syncIdGenerators";
|
|
7
|
+
import { NO_SUBSCRIPTION_PRODUCT_ID, DEFAULT_FREE_USER_DATA } from "./syncConstants";
|
|
8
|
+
|
|
9
|
+
export const handleExpiredSubscription = async (userId: string): Promise<void> => {
|
|
10
|
+
await getCreditsRepository().syncExpiredStatus(userId);
|
|
11
|
+
emitCreditsUpdated(userId);
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const handleFreeUserInitialization = async (userId: string): Promise<void> => {
|
|
15
|
+
const stableSyncId = generateInitSyncId(userId);
|
|
16
|
+
await getCreditsRepository().initializeCredits(
|
|
17
|
+
userId,
|
|
18
|
+
stableSyncId,
|
|
19
|
+
NO_SUBSCRIPTION_PRODUCT_ID,
|
|
20
|
+
PURCHASE_SOURCE.SETTINGS,
|
|
21
|
+
DEFAULT_FREE_USER_DATA,
|
|
22
|
+
PURCHASE_TYPE.INITIAL
|
|
23
|
+
);
|
|
24
|
+
emitCreditsUpdated(userId);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const handlePremiumStatusSync = async (
|
|
28
|
+
userId: string,
|
|
29
|
+
isPremium: boolean,
|
|
30
|
+
productId: string,
|
|
31
|
+
expiresAt: string | null,
|
|
32
|
+
willRenew: boolean,
|
|
33
|
+
periodType: PeriodType | null
|
|
34
|
+
): Promise<void> => {
|
|
35
|
+
const revenueCatData: RevenueCatData = {
|
|
36
|
+
expirationDate: expiresAt,
|
|
37
|
+
willRenew,
|
|
38
|
+
isPremium,
|
|
39
|
+
periodType,
|
|
40
|
+
originalTransactionId: null
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const statusSyncId = generateStatusSyncId(userId, isPremium);
|
|
44
|
+
await getCreditsRepository().initializeCredits(
|
|
45
|
+
userId,
|
|
46
|
+
statusSyncId,
|
|
47
|
+
productId,
|
|
48
|
+
PURCHASE_SOURCE.SETTINGS,
|
|
49
|
+
revenueCatData,
|
|
50
|
+
PURCHASE_TYPE.INITIAL
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
emitCreditsUpdated(userId);
|
|
54
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { RevenueCatData } from "../core/RevenueCatData";
|
|
2
|
+
|
|
3
|
+
export const NO_SUBSCRIPTION_PRODUCT_ID = 'no_subscription';
|
|
4
|
+
|
|
5
|
+
export const DEFAULT_FREE_USER_DATA: RevenueCatData = {
|
|
6
|
+
isPremium: false,
|
|
7
|
+
expirationDate: null,
|
|
8
|
+
willRenew: false,
|
|
9
|
+
periodType: null,
|
|
10
|
+
originalTransactionId: null,
|
|
11
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export const generatePurchaseId = (originalTransactionId: string | null, productId: string): string => {
|
|
2
|
+
return originalTransactionId
|
|
3
|
+
? `purchase_${originalTransactionId}`
|
|
4
|
+
: `purchase_${productId}_${Date.now()}`;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export const generateRenewalId = (originalTransactionId: string | null, productId: string, expirationDate: string): string => {
|
|
8
|
+
return originalTransactionId
|
|
9
|
+
? `renewal_${originalTransactionId}_${expirationDate}`
|
|
10
|
+
: `renewal_${productId}_${Date.now()}`;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const generateInitSyncId = (userId: string): string => {
|
|
14
|
+
return `init_sync_${userId}`;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const generateStatusSyncId = (userId: string, isPremium: boolean): string => {
|
|
18
|
+
return `status_sync_${userId}_${isPremium ? 'premium' : 'free'}`;
|
|
19
|
+
};
|
|
@@ -1,21 +1,13 @@
|
|
|
1
1
|
import type { PurchasesPackage } from "react-native-purchases";
|
|
2
2
|
import type { IRevenueCatService } from "../../../../shared/application/ports/IRevenueCatService";
|
|
3
|
-
import {
|
|
4
|
-
import { PackageHandler } from "../handlers/PackageHandler";
|
|
3
|
+
import type { PackageHandler } from "../handlers/PackageHandler";
|
|
5
4
|
import { SubscriptionInternalState } from "./SubscriptionInternalState";
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
} from "./
|
|
12
|
-
|
|
13
|
-
import type {
|
|
14
|
-
SubscriptionManagerConfig,
|
|
15
|
-
PremiumStatus,
|
|
16
|
-
RestoreResultInfo
|
|
17
|
-
} from "./SubscriptionManager.types";
|
|
18
|
-
|
|
5
|
+
import { ensureConfigured, ensureServiceAvailable } from "./subscriptionManagerUtils";
|
|
6
|
+
import type { SubscriptionManagerConfig, PremiumStatus, RestoreResultInfo } from "./SubscriptionManager.types";
|
|
7
|
+
import { createPackageHandler } from "./packageHandlerFactory";
|
|
8
|
+
import { checkPremiumStatusFromService } from "./premiumStatusChecker";
|
|
9
|
+
import { getPackagesOperation, purchasePackageOperation, restoreOperation } from "./managerOperations";
|
|
10
|
+
import { performServiceInitialization } from "./initializationHandler";
|
|
19
11
|
|
|
20
12
|
class SubscriptionManagerImpl {
|
|
21
13
|
private managerConfig: SubscriptionManagerConfig | null = null;
|
|
@@ -29,17 +21,8 @@ class SubscriptionManagerImpl {
|
|
|
29
21
|
}
|
|
30
22
|
|
|
31
23
|
private ensurePackageHandlerInitialized(): void {
|
|
32
|
-
if (this.packageHandler)
|
|
33
|
-
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
ensureServiceAvailable(this.serviceInstance);
|
|
37
|
-
ensureConfigured(this.managerConfig);
|
|
38
|
-
|
|
39
|
-
this.packageHandler = new PackageHandler(
|
|
40
|
-
this.serviceInstance!,
|
|
41
|
-
this.managerConfig!.config.entitlementIdentifier
|
|
42
|
-
);
|
|
24
|
+
if (this.packageHandler) return;
|
|
25
|
+
this.packageHandler = createPackageHandler(this.serviceInstance, this.managerConfig);
|
|
43
26
|
}
|
|
44
27
|
|
|
45
28
|
async initialize(userId?: string): Promise<boolean> {
|
|
@@ -52,66 +35,41 @@ class SubscriptionManagerImpl {
|
|
|
52
35
|
return existingPromise;
|
|
53
36
|
}
|
|
54
37
|
|
|
55
|
-
const promise = (
|
|
56
|
-
await initializeRevenueCatService(this.managerConfig!.config);
|
|
57
|
-
this.serviceInstance = getRevenueCatService();
|
|
58
|
-
|
|
59
|
-
ensureServiceAvailable(this.serviceInstance);
|
|
60
|
-
this.ensurePackageHandlerInitialized();
|
|
61
|
-
|
|
62
|
-
const result = await this.serviceInstance!.initialize(actualUserId);
|
|
63
|
-
return result.success;
|
|
64
|
-
})();
|
|
65
|
-
|
|
38
|
+
const promise = this.performInitialization(actualUserId);
|
|
66
39
|
this.state.initCache.setPromise(promise, actualUserId);
|
|
67
40
|
return promise;
|
|
68
41
|
}
|
|
69
42
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
return this.state.initCache.getCurrentUserId() === userId;
|
|
43
|
+
private async performInitialization(userId: string): Promise<boolean> {
|
|
44
|
+
const { service, success } = await performServiceInitialization(this.managerConfig!.config, userId);
|
|
45
|
+
this.serviceInstance = service;
|
|
46
|
+
this.ensurePackageHandlerInitialized();
|
|
47
|
+
return success;
|
|
76
48
|
}
|
|
77
49
|
|
|
50
|
+
isInitializedForUser = (userId: string): boolean =>
|
|
51
|
+
this.serviceInstance?.isInitialized() && this.state.initCache.getCurrentUserId() === userId;
|
|
52
|
+
|
|
78
53
|
async getPackages(): Promise<PurchasesPackage[]> {
|
|
79
|
-
ensureConfigured(this.managerConfig);
|
|
80
|
-
this.serviceInstance = getOrCreateService(this.serviceInstance);
|
|
81
54
|
this.ensurePackageHandlerInitialized();
|
|
82
|
-
|
|
83
|
-
return this.packageHandler!.fetchPackages();
|
|
55
|
+
return getPackagesOperation(this.managerConfig, this.serviceInstance, this.packageHandler!);
|
|
84
56
|
}
|
|
85
57
|
|
|
86
58
|
async purchasePackage(pkg: PurchasesPackage): Promise<boolean> {
|
|
87
|
-
ensureConfigured(this.managerConfig);
|
|
88
|
-
const userId = getCurrentUserIdOrThrow(this.state);
|
|
89
59
|
this.ensurePackageHandlerInitialized();
|
|
90
|
-
|
|
91
|
-
return this.packageHandler!.purchase(pkg, userId);
|
|
60
|
+
return purchasePackageOperation(pkg, this.managerConfig, this.state, this.packageHandler!);
|
|
92
61
|
}
|
|
93
62
|
|
|
94
63
|
async restore(): Promise<RestoreResultInfo> {
|
|
95
|
-
ensureConfigured(this.managerConfig);
|
|
96
|
-
const userId = getCurrentUserIdOrThrow(this.state);
|
|
97
64
|
this.ensurePackageHandlerInitialized();
|
|
98
|
-
|
|
99
|
-
return this.packageHandler!.restore(userId);
|
|
65
|
+
return restoreOperation(this.managerConfig, this.state, this.packageHandler!);
|
|
100
66
|
}
|
|
101
67
|
|
|
102
68
|
async checkPremiumStatus(): Promise<PremiumStatus> {
|
|
103
69
|
ensureConfigured(this.managerConfig);
|
|
104
|
-
getCurrentUserIdOrThrow(this.state);
|
|
105
70
|
ensureServiceAvailable(this.serviceInstance);
|
|
106
|
-
|
|
107
|
-
const customerInfo = await this.serviceInstance!.getCustomerInfo();
|
|
108
|
-
|
|
109
|
-
if (!customerInfo) {
|
|
110
|
-
throw new Error("Customer info not available");
|
|
111
|
-
}
|
|
112
|
-
|
|
113
71
|
this.ensurePackageHandlerInitialized();
|
|
114
|
-
return this.packageHandler
|
|
72
|
+
return checkPremiumStatusFromService(this.serviceInstance!, this.packageHandler!);
|
|
115
73
|
}
|
|
116
74
|
|
|
117
75
|
async reset(): Promise<void> {
|
|
@@ -121,13 +79,9 @@ class SubscriptionManagerImpl {
|
|
|
121
79
|
this.packageHandler = null;
|
|
122
80
|
}
|
|
123
81
|
|
|
124
|
-
isConfigured(): boolean
|
|
125
|
-
return this.managerConfig !== null;
|
|
126
|
-
}
|
|
82
|
+
isConfigured = (): boolean => this.managerConfig !== null;
|
|
127
83
|
|
|
128
|
-
isInitialized(): boolean
|
|
129
|
-
return this.serviceInstance?.isInitialized() ?? false;
|
|
130
|
-
}
|
|
84
|
+
isInitialized = (): boolean => this.serviceInstance?.isInitialized() ?? false;
|
|
131
85
|
|
|
132
86
|
getEntitlementId(): string {
|
|
133
87
|
ensureConfigured(this.managerConfig);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { IRevenueCatService } from "../../../../shared/application/ports/IRevenueCatService";
|
|
2
|
+
import { initializeRevenueCatService, getRevenueCatService } from "../services/RevenueCatService";
|
|
3
|
+
import { ensureServiceAvailable } from "./subscriptionManagerUtils";
|
|
4
|
+
import type { RevenueCatConfig } from "../../core/RevenueCatConfig";
|
|
5
|
+
|
|
6
|
+
export const performServiceInitialization = async (config: RevenueCatConfig, userId: string): Promise<{ service: IRevenueCatService; success: boolean }> => {
|
|
7
|
+
await initializeRevenueCatService(config);
|
|
8
|
+
const service = getRevenueCatService();
|
|
9
|
+
|
|
10
|
+
ensureServiceAvailable(service);
|
|
11
|
+
|
|
12
|
+
const result = await service!.initialize(userId);
|
|
13
|
+
return { service: service!, success: result.success };
|
|
14
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { PurchasesPackage } from "react-native-purchases";
|
|
2
|
+
import type { IRevenueCatService } from "../../../../shared/application/ports/IRevenueCatService";
|
|
3
|
+
import type { PackageHandler } from "../handlers/PackageHandler";
|
|
4
|
+
import type { RestoreResultInfo } from "./SubscriptionManager.types";
|
|
5
|
+
import { SubscriptionInternalState } from "./SubscriptionInternalState";
|
|
6
|
+
import { ensureConfigured, getCurrentUserIdOrThrow, getOrCreateService } from "./subscriptionManagerUtils";
|
|
7
|
+
import type { SubscriptionManagerConfig } from "./SubscriptionManager.types";
|
|
8
|
+
|
|
9
|
+
export const getPackagesOperation = async (
|
|
10
|
+
managerConfig: SubscriptionManagerConfig | null,
|
|
11
|
+
serviceInstance: IRevenueCatService | null,
|
|
12
|
+
packageHandler: PackageHandler
|
|
13
|
+
): Promise<PurchasesPackage[]> => {
|
|
14
|
+
ensureConfigured(managerConfig);
|
|
15
|
+
getOrCreateService(serviceInstance);
|
|
16
|
+
return packageHandler.fetchPackages();
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const purchasePackageOperation = async (
|
|
20
|
+
pkg: PurchasesPackage,
|
|
21
|
+
managerConfig: SubscriptionManagerConfig | null,
|
|
22
|
+
state: SubscriptionInternalState,
|
|
23
|
+
packageHandler: PackageHandler
|
|
24
|
+
): Promise<boolean> => {
|
|
25
|
+
ensureConfigured(managerConfig);
|
|
26
|
+
const userId = getCurrentUserIdOrThrow(state);
|
|
27
|
+
return packageHandler.purchase(pkg, userId);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const restoreOperation = async (
|
|
31
|
+
managerConfig: SubscriptionManagerConfig | null,
|
|
32
|
+
state: SubscriptionInternalState,
|
|
33
|
+
packageHandler: PackageHandler
|
|
34
|
+
): Promise<RestoreResultInfo> => {
|
|
35
|
+
ensureConfigured(managerConfig);
|
|
36
|
+
const userId = getCurrentUserIdOrThrow(state);
|
|
37
|
+
return packageHandler.restore(userId);
|
|
38
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { IRevenueCatService } from "../../../../shared/application/ports/IRevenueCatService";
|
|
2
|
+
import { PackageHandler } from "../handlers/PackageHandler";
|
|
3
|
+
import { ensureServiceAvailable, ensureConfigured } from "./subscriptionManagerUtils";
|
|
4
|
+
import type { SubscriptionManagerConfig } from "./SubscriptionManager.types";
|
|
5
|
+
|
|
6
|
+
export const createPackageHandler = (
|
|
7
|
+
service: IRevenueCatService | null,
|
|
8
|
+
config: SubscriptionManagerConfig | null
|
|
9
|
+
): PackageHandler => {
|
|
10
|
+
ensureServiceAvailable(service);
|
|
11
|
+
ensureConfigured(config);
|
|
12
|
+
|
|
13
|
+
return new PackageHandler(
|
|
14
|
+
service!,
|
|
15
|
+
config!.config.entitlementIdentifier
|
|
16
|
+
);
|
|
17
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { IRevenueCatService } from "../../../../shared/application/ports/IRevenueCatService";
|
|
2
|
+
import type { PackageHandler } from "../handlers/PackageHandler";
|
|
3
|
+
import type { PremiumStatus } from "./SubscriptionManager.types";
|
|
4
|
+
import { ERROR_MESSAGES } from "./managerConstants";
|
|
5
|
+
|
|
6
|
+
export const checkPremiumStatusFromService = async (
|
|
7
|
+
service: IRevenueCatService,
|
|
8
|
+
packageHandler: PackageHandler
|
|
9
|
+
): Promise<PremiumStatus> => {
|
|
10
|
+
const customerInfo = await service.getCustomerInfo();
|
|
11
|
+
|
|
12
|
+
if (!customerInfo) {
|
|
13
|
+
throw new Error(ERROR_MESSAGES.CUSTOMER_INFO_NOT_AVAILABLE);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return packageHandler.checkPremiumStatusFromInfo(customerInfo);
|
|
17
|
+
};
|
|
@@ -1,41 +1,17 @@
|
|
|
1
1
|
import Purchases, { type CustomerInfo, type PurchasesOfferings } from "react-native-purchases";
|
|
2
2
|
import type { InitializeResult } from "../../../../shared/application/ports/IRevenueCatService";
|
|
3
|
-
import type { RevenueCatConfig } from "../../core/RevenueCatConfig";
|
|
4
3
|
import { resolveApiKey } from "../utils/ApiKeyResolver";
|
|
4
|
+
import type { InitializerDeps } from "./RevenueCatInitializer.types";
|
|
5
|
+
import { FAILED_INITIALIZATION_RESULT, CONFIGURATION_RETRY_DELAY_MS } from "./initializerConstants";
|
|
5
6
|
|
|
6
|
-
export
|
|
7
|
-
config: RevenueCatConfig;
|
|
8
|
-
isInitialized: () => boolean;
|
|
9
|
-
getCurrentUserId: () => string | null;
|
|
10
|
-
setInitialized: (value: boolean) => void;
|
|
11
|
-
setCurrentUserId: (userId: string) => void;
|
|
12
|
-
}
|
|
7
|
+
export type { InitializerDeps } from "./RevenueCatInitializer.types";
|
|
13
8
|
|
|
14
|
-
// State management to prevent race conditions
|
|
15
9
|
const configurationState = {
|
|
16
10
|
isPurchasesConfigured: false,
|
|
17
|
-
isLogHandlerConfigured: false,
|
|
18
11
|
configurationInProgress: false,
|
|
19
12
|
configurationPromise: null as Promise<InitializeResult> | null,
|
|
20
13
|
};
|
|
21
14
|
|
|
22
|
-
|
|
23
|
-
// Simple lock mechanism to prevent concurrent configurations (implementation deferred)
|
|
24
|
-
|
|
25
|
-
function configureLogHandler(): void {
|
|
26
|
-
if (configurationState.isLogHandlerConfigured) return;
|
|
27
|
-
if (typeof Purchases.setLogHandler !== 'function') return;
|
|
28
|
-
try {
|
|
29
|
-
Purchases.setLogHandler((_logLevel, message) => {
|
|
30
|
-
const ignoreMessages = ['Purchase was cancelled', 'AppTransaction', "Couldn't find previous transactions"];
|
|
31
|
-
if (ignoreMessages.some(m => message.includes(m))) return;
|
|
32
|
-
});
|
|
33
|
-
configurationState.isLogHandlerConfigured = true;
|
|
34
|
-
} catch {
|
|
35
|
-
// Failing to set log handler should not block initialization
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
15
|
function buildSuccessResult(deps: InitializerDeps, customerInfo: CustomerInfo, offerings: PurchasesOfferings): InitializeResult {
|
|
40
16
|
const isPremium = !!customerInfo.entitlements.active[deps.config.entitlementIdentifier];
|
|
41
17
|
return { success: true, offering: offerings.current, isPremium };
|
|
@@ -54,7 +30,7 @@ export async function initializeSDK(
|
|
|
54
30
|
]);
|
|
55
31
|
return buildSuccessResult(deps, customerInfo, offerings);
|
|
56
32
|
} catch {
|
|
57
|
-
return
|
|
33
|
+
return FAILED_INITIALIZATION_RESULT;
|
|
58
34
|
}
|
|
59
35
|
}
|
|
60
36
|
|
|
@@ -73,7 +49,7 @@ export async function initializeSDK(
|
|
|
73
49
|
const offerings = await Purchases.getOfferings();
|
|
74
50
|
return buildSuccessResult(deps, customerInfo, offerings);
|
|
75
51
|
} catch {
|
|
76
|
-
return
|
|
52
|
+
return FAILED_INITIALIZATION_RESULT;
|
|
77
53
|
}
|
|
78
54
|
}
|
|
79
55
|
|
|
@@ -82,13 +58,13 @@ export async function initializeSDK(
|
|
|
82
58
|
await configurationState.configurationPromise;
|
|
83
59
|
return initializeSDK(deps, userId, apiKey);
|
|
84
60
|
}
|
|
85
|
-
await new Promise(resolve => setTimeout(resolve,
|
|
61
|
+
await new Promise(resolve => setTimeout(resolve, CONFIGURATION_RETRY_DELAY_MS));
|
|
86
62
|
return initializeSDK(deps, userId, apiKey);
|
|
87
63
|
}
|
|
88
64
|
|
|
89
65
|
const key = apiKey || resolveApiKey(deps.config);
|
|
90
66
|
if (!key) {
|
|
91
|
-
return
|
|
67
|
+
return FAILED_INITIALIZATION_RESULT;
|
|
92
68
|
}
|
|
93
69
|
|
|
94
70
|
let resolveInProgress: (value: InitializeResult) => void;
|
|
@@ -98,7 +74,6 @@ export async function initializeSDK(
|
|
|
98
74
|
|
|
99
75
|
configurationState.configurationInProgress = true;
|
|
100
76
|
try {
|
|
101
|
-
configureLogHandler();
|
|
102
77
|
await Purchases.configure({ apiKey: key, appUserID: userId });
|
|
103
78
|
configurationState.isPurchasesConfigured = true;
|
|
104
79
|
deps.setInitialized(true);
|
|
@@ -113,9 +88,8 @@ export async function initializeSDK(
|
|
|
113
88
|
resolveInProgress!(result);
|
|
114
89
|
return result;
|
|
115
90
|
} catch {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
return errorResult;
|
|
91
|
+
resolveInProgress!(FAILED_INITIALIZATION_RESULT);
|
|
92
|
+
return FAILED_INITIALIZATION_RESULT;
|
|
119
93
|
} finally {
|
|
120
94
|
configurationState.configurationInProgress = false;
|
|
121
95
|
configurationState.configurationPromise = null;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { RevenueCatConfig } from "../../core/RevenueCatConfig";
|
|
2
|
+
|
|
3
|
+
export interface InitializerDeps {
|
|
4
|
+
config: RevenueCatConfig;
|
|
5
|
+
isInitialized: () => boolean;
|
|
6
|
+
getCurrentUserId: () => string | null;
|
|
7
|
+
setInitialized: (value: boolean) => void;
|
|
8
|
+
setCurrentUserId: (userId: string) => void;
|
|
9
|
+
}
|
|
@@ -1,134 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
* Main service class for RevenueCat operations
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import Purchases from "react-native-purchases";
|
|
7
|
-
import type {
|
|
8
|
-
PurchasesOffering,
|
|
9
|
-
PurchasesPackage,
|
|
10
|
-
CustomerInfo,
|
|
11
|
-
} from "react-native-purchases";
|
|
12
|
-
import type {
|
|
13
|
-
IRevenueCatService,
|
|
14
|
-
InitializeResult,
|
|
15
|
-
PurchaseResult,
|
|
16
|
-
RestoreResult,
|
|
17
|
-
} from "../../../../shared/application/ports/IRevenueCatService";
|
|
18
|
-
import type { RevenueCatConfig } from "../../core/RevenueCatConfig";
|
|
19
|
-
import { resolveApiKey } from "../utils/ApiKeyResolver";
|
|
20
|
-
import { initializeSDK } from "./RevenueCatInitializer";
|
|
21
|
-
import { fetchOfferings } from "./OfferingsFetcher";
|
|
22
|
-
import { handlePurchase } from "./PurchaseHandler";
|
|
23
|
-
import { handleRestore } from "./RestoreHandler";
|
|
24
|
-
import { CustomerInfoListenerManager } from "./CustomerInfoListenerManager";
|
|
25
|
-
import { ServiceStateManager } from "./ServiceStateManager";
|
|
26
|
-
|
|
27
|
-
export class RevenueCatService implements IRevenueCatService {
|
|
28
|
-
private stateManager: ServiceStateManager;
|
|
29
|
-
private listenerManager: CustomerInfoListenerManager;
|
|
30
|
-
|
|
31
|
-
constructor(config: RevenueCatConfig) {
|
|
32
|
-
this.stateManager = new ServiceStateManager(config);
|
|
33
|
-
this.listenerManager = new CustomerInfoListenerManager();
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
getRevenueCatKey(): string | null {
|
|
37
|
-
return resolveApiKey(this.stateManager.getConfig());
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
isInitialized(): boolean {
|
|
41
|
-
return this.stateManager.isInitialized();
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
getCurrentUserId(): string | null {
|
|
45
|
-
return this.stateManager.getCurrentUserId();
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
private getSDKParams() {
|
|
49
|
-
return {
|
|
50
|
-
config: this.stateManager.getConfig(),
|
|
51
|
-
isInitialized: () => this.isInitialized(),
|
|
52
|
-
getCurrentUserId: () => this.stateManager.getCurrentUserId(),
|
|
53
|
-
setInitialized: (value: boolean) => this.stateManager.setInitialized(value),
|
|
54
|
-
setCurrentUserId: (id: string | null) => this.stateManager.setCurrentUserId(id),
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
async initialize(userId: string, apiKey?: string): Promise<InitializeResult> {
|
|
59
|
-
if (this.isInitialized() && this.getCurrentUserId() === userId) {
|
|
60
|
-
const customerInfo = await Purchases.getCustomerInfo();
|
|
61
|
-
const isPremium = !!customerInfo.entitlements.active[this.stateManager.getConfig().entitlementIdentifier];
|
|
62
|
-
return {
|
|
63
|
-
success: true,
|
|
64
|
-
offering: await this.fetchOfferings(),
|
|
65
|
-
isPremium,
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const result = await initializeSDK(this.getSDKParams(), userId, apiKey);
|
|
70
|
-
|
|
71
|
-
if (result.success) {
|
|
72
|
-
this.listenerManager.setUserId(userId);
|
|
73
|
-
this.listenerManager.setupListener(this.stateManager.getConfig());
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
return result;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
async fetchOfferings(): Promise<PurchasesOffering | null> {
|
|
80
|
-
return fetchOfferings(this.getSDKParams());
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
async purchasePackage(
|
|
84
|
-
pkg: PurchasesPackage,
|
|
85
|
-
userId: string
|
|
86
|
-
): Promise<PurchaseResult> {
|
|
87
|
-
return handlePurchase(this.getSDKParams(), pkg, userId);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
async restorePurchases(userId: string): Promise<RestoreResult> {
|
|
91
|
-
return handleRestore(this.getSDKParams(), userId);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
async getCustomerInfo(): Promise<CustomerInfo | null> {
|
|
95
|
-
if (!this.isInitialized()) {
|
|
96
|
-
return null;
|
|
97
|
-
}
|
|
98
|
-
return Purchases.getCustomerInfo();
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
async reset(): Promise<void> {
|
|
102
|
-
if (!this.isInitialized()) {
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
this.listenerManager.destroy();
|
|
107
|
-
|
|
108
|
-
try {
|
|
109
|
-
await Purchases.logOut();
|
|
110
|
-
this.stateManager.setInitialized(false);
|
|
111
|
-
} catch {
|
|
112
|
-
// Silently fail during logout to allow cleanup to complete
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
let revenueCatServiceInstance: RevenueCatService | null = null;
|
|
118
|
-
|
|
119
|
-
export function initializeRevenueCatService(
|
|
120
|
-
config: RevenueCatConfig
|
|
121
|
-
): RevenueCatService {
|
|
122
|
-
if (!revenueCatServiceInstance) {
|
|
123
|
-
revenueCatServiceInstance = new RevenueCatService(config);
|
|
124
|
-
}
|
|
125
|
-
return revenueCatServiceInstance;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
export function getRevenueCatService(): RevenueCatService | null {
|
|
129
|
-
return revenueCatServiceInstance;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
export function resetRevenueCatService(): void {
|
|
133
|
-
revenueCatServiceInstance = null;
|
|
134
|
-
}
|
|
1
|
+
export { RevenueCatService } from "./RevenueCatService.types";
|
|
2
|
+
export { initializeRevenueCatService, getRevenueCatService, resetRevenueCatService } from "./revenueCatServiceInstance";
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import Purchases from "react-native-purchases";
|
|
2
|
+
import type { PurchasesOffering, PurchasesPackage, CustomerInfo } from "react-native-purchases";
|
|
3
|
+
import type { IRevenueCatService, InitializeResult, PurchaseResult, RestoreResult } from "../../../../shared/application/ports/IRevenueCatService";
|
|
4
|
+
import type { RevenueCatConfig } from "../../core/RevenueCatConfig";
|
|
5
|
+
import { resolveApiKey } from "../utils/ApiKeyResolver";
|
|
6
|
+
import { initializeSDK } from "./RevenueCatInitializer";
|
|
7
|
+
import { fetchOfferings } from "./OfferingsFetcher";
|
|
8
|
+
import { handlePurchase } from "./PurchaseHandler";
|
|
9
|
+
import { handleRestore } from "./RestoreHandler";
|
|
10
|
+
import { CustomerInfoListenerManager } from "./CustomerInfoListenerManager";
|
|
11
|
+
import { ServiceStateManager } from "./ServiceStateManager";
|
|
12
|
+
|
|
13
|
+
export class RevenueCatService implements IRevenueCatService {
|
|
14
|
+
private stateManager: ServiceStateManager;
|
|
15
|
+
private listenerManager: CustomerInfoListenerManager;
|
|
16
|
+
|
|
17
|
+
constructor(config: RevenueCatConfig) {
|
|
18
|
+
this.stateManager = new ServiceStateManager(config);
|
|
19
|
+
this.listenerManager = new CustomerInfoListenerManager();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
getRevenueCatKey = (): string | null => resolveApiKey(this.stateManager.getConfig());
|
|
23
|
+
|
|
24
|
+
isInitialized = (): boolean => this.stateManager.isInitialized();
|
|
25
|
+
|
|
26
|
+
getCurrentUserId = (): string | null => this.stateManager.getCurrentUserId();
|
|
27
|
+
|
|
28
|
+
private getSDKParams() {
|
|
29
|
+
return {
|
|
30
|
+
config: this.stateManager.getConfig(),
|
|
31
|
+
isInitialized: () => this.isInitialized(),
|
|
32
|
+
getCurrentUserId: () => this.stateManager.getCurrentUserId(),
|
|
33
|
+
setInitialized: (value: boolean) => this.stateManager.setInitialized(value),
|
|
34
|
+
setCurrentUserId: (id: string | null) => this.stateManager.setCurrentUserId(id),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async initialize(userId: string, apiKey?: string): Promise<InitializeResult> {
|
|
39
|
+
if (this.isInitialized() && this.getCurrentUserId() === userId) {
|
|
40
|
+
const customerInfo = await Purchases.getCustomerInfo();
|
|
41
|
+
const isPremium = !!customerInfo.entitlements.active[this.stateManager.getConfig().entitlementIdentifier];
|
|
42
|
+
return {
|
|
43
|
+
success: true,
|
|
44
|
+
offering: await this.fetchOfferings(),
|
|
45
|
+
isPremium,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const result = await initializeSDK(this.getSDKParams(), userId, apiKey);
|
|
50
|
+
|
|
51
|
+
if (result.success) {
|
|
52
|
+
this.listenerManager.setUserId(userId);
|
|
53
|
+
this.listenerManager.setupListener(this.stateManager.getConfig());
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async fetchOfferings(): Promise<PurchasesOffering | null> {
|
|
60
|
+
return fetchOfferings(this.getSDKParams());
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async purchasePackage(pkg: PurchasesPackage, userId: string): Promise<PurchaseResult> {
|
|
64
|
+
return handlePurchase(this.getSDKParams(), pkg, userId);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async restorePurchases(userId: string): Promise<RestoreResult> {
|
|
68
|
+
return handleRestore(this.getSDKParams(), userId);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async getCustomerInfo(): Promise<CustomerInfo | null> {
|
|
72
|
+
if (!this.isInitialized()) return null;
|
|
73
|
+
return Purchases.getCustomerInfo();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async reset(): Promise<void> {
|
|
77
|
+
if (!this.isInitialized()) return;
|
|
78
|
+
|
|
79
|
+
this.listenerManager.destroy();
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
await Purchases.logOut();
|
|
83
|
+
this.stateManager.setInitialized(false);
|
|
84
|
+
} catch {
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { InitializeResult } from "../../../../shared/application/ports/IRevenueCatService";
|
|
2
|
+
|
|
3
|
+
export const FAILED_INITIALIZATION_RESULT: InitializeResult = {
|
|
4
|
+
success: false,
|
|
5
|
+
offering: null,
|
|
6
|
+
isPremium: false,
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export const CONFIGURATION_RETRY_DELAY_MS = 100;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { RevenueCatService } from "./RevenueCatService.types";
|
|
2
|
+
import type { RevenueCatConfig } from "../../core/RevenueCatConfig";
|
|
3
|
+
|
|
4
|
+
let revenueCatServiceInstance: RevenueCatService | null = null;
|
|
5
|
+
|
|
6
|
+
export const initializeRevenueCatService = (config: RevenueCatConfig): RevenueCatService => {
|
|
7
|
+
if (!revenueCatServiceInstance) {
|
|
8
|
+
revenueCatServiceInstance = new RevenueCatService(config);
|
|
9
|
+
}
|
|
10
|
+
return revenueCatServiceInstance;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const getRevenueCatService = (): RevenueCatService | null => revenueCatServiceInstance;
|
|
14
|
+
|
|
15
|
+
export const resetRevenueCatService = (): void => {
|
|
16
|
+
revenueCatServiceInstance = null;
|
|
17
|
+
};
|