@umituz/react-native-subscription 2.4.0 → 2.6.0
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 +3 -1
- package/src/index.ts +6 -0
- package/src/revenuecat/infrastructure/handlers/PackageHandler.ts +128 -0
- package/src/revenuecat/infrastructure/managers/SubscriptionManager.ts +153 -0
- package/src/revenuecat/infrastructure/services/PurchaseHandler.ts +38 -6
- package/src/revenuecat/infrastructure/services/RestoreHandler.ts +30 -4
- package/src/revenuecat/infrastructure/services/RevenueCatService.ts +50 -23
- package/src/revenuecat/infrastructure/utils/InitializationCache.ts +45 -0
- package/src/revenuecat/infrastructure/utils/UserIdProvider.ts +45 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-subscription",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.6.0",
|
|
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",
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
"@umituz/react-native-firestore": "latest",
|
|
36
36
|
"@umituz/react-native-legal": "latest",
|
|
37
37
|
"@umituz/react-native-localization": "latest",
|
|
38
|
+
"@umituz/react-native-sentry": "latest",
|
|
38
39
|
"expo-constants": ">=16.0.0",
|
|
39
40
|
"firebase": ">=10.0.0",
|
|
40
41
|
"react": ">=18.2.0",
|
|
@@ -47,6 +48,7 @@
|
|
|
47
48
|
"@umituz/react-native-firestore": "latest",
|
|
48
49
|
"@umituz/react-native-legal": "latest",
|
|
49
50
|
"@umituz/react-native-localization": "latest",
|
|
51
|
+
"@umituz/react-native-sentry": "latest",
|
|
50
52
|
"@tanstack/react-query": "^5.0.0",
|
|
51
53
|
"expo-constants": "~16.0.0",
|
|
52
54
|
"firebase": "^10.0.0",
|
package/src/index.ts
CHANGED
|
@@ -305,6 +305,12 @@ export {
|
|
|
305
305
|
resetRevenueCatService,
|
|
306
306
|
} from "./revenuecat/infrastructure/services/RevenueCatService";
|
|
307
307
|
|
|
308
|
+
export {
|
|
309
|
+
SubscriptionManager,
|
|
310
|
+
type SubscriptionManagerConfig,
|
|
311
|
+
type PremiumStatus,
|
|
312
|
+
} from "./revenuecat/infrastructure/managers/SubscriptionManager";
|
|
313
|
+
|
|
308
314
|
// =============================================================================
|
|
309
315
|
// REVENUECAT - Hooks
|
|
310
316
|
// =============================================================================
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package Handler
|
|
3
|
+
* Handles package operations (fetch, purchase, restore, premium status)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { PurchasesPackage } from "react-native-purchases";
|
|
7
|
+
import type { IRevenueCatService } from "../../application/ports/IRevenueCatService";
|
|
8
|
+
import { getPremiumEntitlement } from "../../domain/types/RevenueCatTypes";
|
|
9
|
+
import {
|
|
10
|
+
trackPackageError,
|
|
11
|
+
addPackageBreadcrumb,
|
|
12
|
+
trackPackageWarning,
|
|
13
|
+
} from "@umituz/react-native-sentry";
|
|
14
|
+
|
|
15
|
+
export interface PremiumStatus {
|
|
16
|
+
isPremium: boolean;
|
|
17
|
+
expirationDate: Date | null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class PackageHandler {
|
|
21
|
+
constructor(
|
|
22
|
+
private service: IRevenueCatService | null,
|
|
23
|
+
private entitlementId: string
|
|
24
|
+
) {}
|
|
25
|
+
|
|
26
|
+
setService(service: IRevenueCatService | null): void {
|
|
27
|
+
this.service = service;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async fetchPackages(): Promise<PurchasesPackage[]> {
|
|
31
|
+
if (!this.service?.isInitialized()) {
|
|
32
|
+
trackPackageWarning("subscription", "Fetch packages called but not initialized", {});
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
const offering = await this.service.fetchOfferings();
|
|
38
|
+
|
|
39
|
+
addPackageBreadcrumb("subscription", "Packages fetched", {
|
|
40
|
+
identifier: offering?.identifier,
|
|
41
|
+
count: offering?.availablePackages?.length ?? 0,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
return offering?.availablePackages ?? [];
|
|
45
|
+
} catch (error) {
|
|
46
|
+
trackPackageError(error instanceof Error ? error : new Error(String(error)), {
|
|
47
|
+
packageName: "subscription",
|
|
48
|
+
operation: "fetch_packages",
|
|
49
|
+
});
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async purchase(pkg: PurchasesPackage, userId: string): Promise<boolean> {
|
|
55
|
+
if (!this.service?.isInitialized()) {
|
|
56
|
+
trackPackageWarning("subscription", "Purchase attempted but not initialized", {
|
|
57
|
+
productId: pkg.product.identifier,
|
|
58
|
+
});
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
const result = await this.service.purchasePackage(pkg, userId);
|
|
64
|
+
return result.success;
|
|
65
|
+
} catch (error) {
|
|
66
|
+
trackPackageError(error instanceof Error ? error : new Error(String(error)), {
|
|
67
|
+
packageName: "subscription",
|
|
68
|
+
operation: "purchase",
|
|
69
|
+
userId,
|
|
70
|
+
productId: pkg.product.identifier,
|
|
71
|
+
});
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async restore(userId: string): Promise<boolean> {
|
|
77
|
+
if (!this.service?.isInitialized()) {
|
|
78
|
+
trackPackageWarning("subscription", "Restore attempted but not initialized", {});
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
const result = await this.service.restorePurchases(userId);
|
|
84
|
+
return result.success;
|
|
85
|
+
} catch (error) {
|
|
86
|
+
trackPackageError(error instanceof Error ? error : new Error(String(error)), {
|
|
87
|
+
packageName: "subscription",
|
|
88
|
+
operation: "restore",
|
|
89
|
+
userId,
|
|
90
|
+
});
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async checkPremiumStatus(userId: string): Promise<PremiumStatus> {
|
|
96
|
+
if (!this.service?.isInitialized()) {
|
|
97
|
+
return { isPremium: false, expirationDate: null };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
const restoreResult = await this.service.restorePurchases(userId);
|
|
102
|
+
|
|
103
|
+
if (restoreResult.customerInfo) {
|
|
104
|
+
const entitlement = getPremiumEntitlement(
|
|
105
|
+
restoreResult.customerInfo,
|
|
106
|
+
this.entitlementId
|
|
107
|
+
);
|
|
108
|
+
if (entitlement) {
|
|
109
|
+
return {
|
|
110
|
+
isPremium: true,
|
|
111
|
+
expirationDate: entitlement.expirationDate
|
|
112
|
+
? new Date(entitlement.expirationDate)
|
|
113
|
+
: null,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return { isPremium: restoreResult.isPremium, expirationDate: null };
|
|
119
|
+
} catch (error) {
|
|
120
|
+
trackPackageError(error instanceof Error ? error : new Error(String(error)), {
|
|
121
|
+
packageName: "subscription",
|
|
122
|
+
operation: "check_premium_status",
|
|
123
|
+
userId,
|
|
124
|
+
});
|
|
125
|
+
return { isPremium: false, expirationDate: null };
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subscription Manager
|
|
3
|
+
* Facade for subscription operations
|
|
4
|
+
* Coordinates UserIdProvider, InitializationCache, and PackageHandler
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { PurchasesPackage } from "react-native-purchases";
|
|
8
|
+
import type { RevenueCatConfig } from "../../domain/value-objects/RevenueCatConfig";
|
|
9
|
+
import type { IRevenueCatService } from "../../application/ports/IRevenueCatService";
|
|
10
|
+
import { initializeRevenueCatService, getRevenueCatService } from "../services/RevenueCatService";
|
|
11
|
+
import { UserIdProvider } from "../utils/UserIdProvider";
|
|
12
|
+
import { InitializationCache } from "../utils/InitializationCache";
|
|
13
|
+
import { PackageHandler, type PremiumStatus } from "../handlers/PackageHandler";
|
|
14
|
+
import {
|
|
15
|
+
trackPackageError,
|
|
16
|
+
addPackageBreadcrumb,
|
|
17
|
+
trackPackageWarning,
|
|
18
|
+
} from "@umituz/react-native-sentry";
|
|
19
|
+
|
|
20
|
+
export interface SubscriptionManagerConfig {
|
|
21
|
+
config: RevenueCatConfig;
|
|
22
|
+
apiKey: string;
|
|
23
|
+
getAnonymousUserId?: () => Promise<string>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
class SubscriptionManagerImpl {
|
|
27
|
+
private managerConfig: SubscriptionManagerConfig | null = null;
|
|
28
|
+
private serviceInstance: IRevenueCatService | null = null;
|
|
29
|
+
private userIdProvider = new UserIdProvider();
|
|
30
|
+
private initCache = new InitializationCache();
|
|
31
|
+
private packageHandler: PackageHandler | null = null;
|
|
32
|
+
|
|
33
|
+
configure(config: SubscriptionManagerConfig): void {
|
|
34
|
+
this.managerConfig = config;
|
|
35
|
+
this.packageHandler = new PackageHandler(null, config.config.entitlementIdentifier);
|
|
36
|
+
|
|
37
|
+
if (config.getAnonymousUserId) {
|
|
38
|
+
this.userIdProvider.configure(config.getAnonymousUserId);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
addPackageBreadcrumb("subscription", "Manager configured", {
|
|
42
|
+
entitlementId: config.config.entitlementIdentifier,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
private ensureConfigured(): void {
|
|
47
|
+
if (!this.managerConfig || !this.packageHandler) {
|
|
48
|
+
const error = new Error("SubscriptionManager not configured");
|
|
49
|
+
trackPackageError(error, {
|
|
50
|
+
packageName: "subscription",
|
|
51
|
+
operation: "ensure_configured",
|
|
52
|
+
});
|
|
53
|
+
throw error;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
private async performInitialization(userId: string): Promise<boolean> {
|
|
58
|
+
this.ensureConfigured();
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
await initializeRevenueCatService(this.managerConfig!.config);
|
|
62
|
+
this.serviceInstance = getRevenueCatService();
|
|
63
|
+
|
|
64
|
+
if (!this.serviceInstance) {
|
|
65
|
+
trackPackageWarning("subscription", "Service instance not created", { userId });
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
this.packageHandler!.setService(this.serviceInstance);
|
|
70
|
+
|
|
71
|
+
const result = await this.serviceInstance.initialize(userId, this.managerConfig!.apiKey);
|
|
72
|
+
return result.success;
|
|
73
|
+
} catch (error) {
|
|
74
|
+
trackPackageError(error instanceof Error ? error : new Error(String(error)), {
|
|
75
|
+
packageName: "subscription",
|
|
76
|
+
operation: "initialize",
|
|
77
|
+
userId,
|
|
78
|
+
});
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async initialize(userId?: string): Promise<boolean> {
|
|
84
|
+
this.ensureConfigured();
|
|
85
|
+
|
|
86
|
+
const effectiveUserId = userId || (await this.userIdProvider.getOrCreateAnonymousUserId());
|
|
87
|
+
|
|
88
|
+
if (!this.initCache.shouldReinitialize(effectiveUserId)) {
|
|
89
|
+
const existingPromise = this.initCache.getExistingPromise();
|
|
90
|
+
if (existingPromise) return existingPromise;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (this.initCache.shouldReinitialize(effectiveUserId) && this.serviceInstance) {
|
|
94
|
+
await this.serviceInstance.reset();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const promise = this.performInitialization(effectiveUserId);
|
|
98
|
+
this.initCache.setPromise(promise, effectiveUserId);
|
|
99
|
+
|
|
100
|
+
return promise;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
isInitialized(): boolean {
|
|
104
|
+
return this.serviceInstance?.isInitialized() ?? false;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
isInitializedForUser(userId: string): boolean {
|
|
108
|
+
return this.serviceInstance?.isInitialized() === true &&
|
|
109
|
+
this.initCache.getCurrentUserId() === userId;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async getPackages(): Promise<PurchasesPackage[]> {
|
|
113
|
+
this.ensureConfigured();
|
|
114
|
+
if (!this.serviceInstance) {
|
|
115
|
+
this.serviceInstance = getRevenueCatService();
|
|
116
|
+
this.packageHandler!.setService(this.serviceInstance);
|
|
117
|
+
}
|
|
118
|
+
return this.packageHandler!.fetchPackages();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async purchasePackage(pkg: PurchasesPackage): Promise<boolean> {
|
|
122
|
+
this.ensureConfigured();
|
|
123
|
+
const userId = this.initCache.getCurrentUserId();
|
|
124
|
+
if (!userId) return false;
|
|
125
|
+
return this.packageHandler!.purchase(pkg, userId);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async restore(): Promise<boolean> {
|
|
129
|
+
this.ensureConfigured();
|
|
130
|
+
const userId = this.initCache.getCurrentUserId();
|
|
131
|
+
if (!userId) return false;
|
|
132
|
+
return this.packageHandler!.restore(userId);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async checkPremiumStatus(): Promise<PremiumStatus> {
|
|
136
|
+
this.ensureConfigured();
|
|
137
|
+
const userId = this.initCache.getCurrentUserId();
|
|
138
|
+
if (!userId) return { isPremium: false, expirationDate: null };
|
|
139
|
+
return this.packageHandler!.checkPremiumStatus(userId);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async reset(): Promise<void> {
|
|
143
|
+
if (this.serviceInstance) {
|
|
144
|
+
await this.serviceInstance.reset();
|
|
145
|
+
}
|
|
146
|
+
this.initCache.reset();
|
|
147
|
+
this.userIdProvider.reset();
|
|
148
|
+
this.serviceInstance = null;
|
|
149
|
+
addPackageBreadcrumb("subscription", "Manager reset completed", {});
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export const SubscriptionManager = new SubscriptionManagerImpl();
|
|
@@ -18,6 +18,10 @@ import {
|
|
|
18
18
|
syncPremiumStatus,
|
|
19
19
|
notifyPurchaseCompleted,
|
|
20
20
|
} from "../utils/PremiumStatusSyncer";
|
|
21
|
+
import {
|
|
22
|
+
trackPackageError,
|
|
23
|
+
addPackageBreadcrumb,
|
|
24
|
+
} from "@umituz/react-native-sentry";
|
|
21
25
|
|
|
22
26
|
export interface PurchaseHandlerDeps {
|
|
23
27
|
config: RevenueCatConfig;
|
|
@@ -42,12 +46,20 @@ export async function handlePurchase(
|
|
|
42
46
|
pkg: PurchasesPackage,
|
|
43
47
|
userId: string
|
|
44
48
|
): Promise<PurchaseResult> {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
49
|
+
addPackageBreadcrumb("subscription", "Purchase started", {
|
|
50
|
+
productId: pkg.product.identifier,
|
|
51
|
+
userId,
|
|
52
|
+
});
|
|
48
53
|
|
|
49
54
|
if (!deps.isInitialized()) {
|
|
50
|
-
|
|
55
|
+
const error = new RevenueCatInitializationError();
|
|
56
|
+
trackPackageError(error, {
|
|
57
|
+
packageName: "subscription",
|
|
58
|
+
operation: "purchase",
|
|
59
|
+
userId,
|
|
60
|
+
productId: pkg.product.identifier,
|
|
61
|
+
});
|
|
62
|
+
throw error;
|
|
51
63
|
}
|
|
52
64
|
|
|
53
65
|
|
|
@@ -85,15 +97,35 @@ export async function handlePurchase(
|
|
|
85
97
|
return { success: true, isPremium: true, customerInfo };
|
|
86
98
|
}
|
|
87
99
|
|
|
88
|
-
|
|
100
|
+
const entitlementError = new RevenueCatPurchaseError(
|
|
89
101
|
"Purchase completed but premium entitlement not active",
|
|
90
102
|
pkg.product.identifier
|
|
91
103
|
);
|
|
104
|
+
trackPackageError(entitlementError, {
|
|
105
|
+
packageName: "subscription",
|
|
106
|
+
operation: "purchase",
|
|
107
|
+
userId,
|
|
108
|
+
productId: pkg.product.identifier,
|
|
109
|
+
reason: "entitlement_not_active",
|
|
110
|
+
});
|
|
111
|
+
throw entitlementError;
|
|
92
112
|
} catch (error) {
|
|
93
113
|
if (isUserCancelledError(error)) {
|
|
114
|
+
addPackageBreadcrumb("subscription", "Purchase cancelled by user", {
|
|
115
|
+
productId: pkg.product.identifier,
|
|
116
|
+
userId,
|
|
117
|
+
});
|
|
94
118
|
return { success: false, isPremium: false };
|
|
95
119
|
}
|
|
96
120
|
const errorMessage = getErrorMessage(error, "Purchase failed");
|
|
97
|
-
|
|
121
|
+
const purchaseError = new RevenueCatPurchaseError(errorMessage, pkg.product.identifier);
|
|
122
|
+
trackPackageError(purchaseError, {
|
|
123
|
+
packageName: "subscription",
|
|
124
|
+
operation: "purchase",
|
|
125
|
+
userId,
|
|
126
|
+
productId: pkg.product.identifier,
|
|
127
|
+
originalError: error instanceof Error ? error.message : String(error),
|
|
128
|
+
});
|
|
129
|
+
throw purchaseError;
|
|
98
130
|
}
|
|
99
131
|
}
|
|
@@ -15,6 +15,10 @@ import {
|
|
|
15
15
|
syncPremiumStatus,
|
|
16
16
|
notifyRestoreCompleted,
|
|
17
17
|
} from "../utils/PremiumStatusSyncer";
|
|
18
|
+
import {
|
|
19
|
+
trackPackageError,
|
|
20
|
+
addPackageBreadcrumb,
|
|
21
|
+
} from "@umituz/react-native-sentry";
|
|
18
22
|
|
|
19
23
|
export interface RestoreHandlerDeps {
|
|
20
24
|
config: RevenueCatConfig;
|
|
@@ -29,12 +33,18 @@ export async function handleRestore(
|
|
|
29
33
|
deps: RestoreHandlerDeps,
|
|
30
34
|
userId: string
|
|
31
35
|
): Promise<RestoreResult> {
|
|
36
|
+
addPackageBreadcrumb("subscription", "Restore started", { userId });
|
|
37
|
+
|
|
32
38
|
if (!deps.isInitialized()) {
|
|
33
|
-
|
|
39
|
+
const error = new RevenueCatInitializationError();
|
|
40
|
+
trackPackageError(error, {
|
|
41
|
+
packageName: "subscription",
|
|
42
|
+
operation: "restore",
|
|
43
|
+
userId,
|
|
44
|
+
});
|
|
45
|
+
throw error;
|
|
34
46
|
}
|
|
35
47
|
|
|
36
|
-
|
|
37
|
-
|
|
38
48
|
try {
|
|
39
49
|
const customerInfo = await Purchases.restorePurchases();
|
|
40
50
|
const entitlementIdentifier = deps.config.entitlementIdentifier;
|
|
@@ -42,12 +52,28 @@ export async function handleRestore(
|
|
|
42
52
|
|
|
43
53
|
if (isPremium) {
|
|
44
54
|
await syncPremiumStatus(deps.config, userId, customerInfo);
|
|
55
|
+
addPackageBreadcrumb("subscription", "Restore successful - premium active", {
|
|
56
|
+
userId,
|
|
57
|
+
entitlementId: entitlementIdentifier,
|
|
58
|
+
});
|
|
59
|
+
} else {
|
|
60
|
+
addPackageBreadcrumb("subscription", "Restore completed - no premium found", {
|
|
61
|
+
userId,
|
|
62
|
+
});
|
|
45
63
|
}
|
|
64
|
+
|
|
46
65
|
await notifyRestoreCompleted(deps.config, userId, isPremium, customerInfo);
|
|
47
66
|
|
|
48
67
|
return { success: isPremium, isPremium, customerInfo };
|
|
49
68
|
} catch (error) {
|
|
50
69
|
const errorMessage = getErrorMessage(error, "Restore failed");
|
|
51
|
-
|
|
70
|
+
const restoreError = new RevenueCatRestoreError(errorMessage);
|
|
71
|
+
trackPackageError(restoreError, {
|
|
72
|
+
packageName: "subscription",
|
|
73
|
+
operation: "restore",
|
|
74
|
+
userId,
|
|
75
|
+
originalError: error instanceof Error ? error.message : String(error),
|
|
76
|
+
});
|
|
77
|
+
throw restoreError;
|
|
52
78
|
}
|
|
53
79
|
}
|
|
@@ -19,6 +19,11 @@ import { handlePurchase } from "./PurchaseHandler";
|
|
|
19
19
|
import { handleRestore } from "./RestoreHandler";
|
|
20
20
|
import { CustomerInfoListenerManager } from "./CustomerInfoListenerManager";
|
|
21
21
|
import { ServiceStateManager } from "./ServiceStateManager";
|
|
22
|
+
import {
|
|
23
|
+
trackPackageError,
|
|
24
|
+
addPackageBreadcrumb,
|
|
25
|
+
trackPackageWarning,
|
|
26
|
+
} from "@umituz/react-native-sentry";
|
|
22
27
|
|
|
23
28
|
export class RevenueCatService implements IRevenueCatService {
|
|
24
29
|
private stateManager: ServiceStateManager;
|
|
@@ -50,31 +55,46 @@ export class RevenueCatService implements IRevenueCatService {
|
|
|
50
55
|
async initialize(userId: string, apiKey?: string): Promise<InitializeResult> {
|
|
51
56
|
// If already initialized for this user, return success immediately
|
|
52
57
|
if (this.isInitialized() && this.getCurrentUserId() === userId) {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}
|
|
56
|
-
return { success: true, offering: (await this.fetchOfferings()), hasPremium: false }; // fetchOfferings handles cache usually
|
|
58
|
+
addPackageBreadcrumb("subscription", "Already initialized", { userId });
|
|
59
|
+
return { success: true, offering: (await this.fetchOfferings()), hasPremium: false };
|
|
57
60
|
}
|
|
58
61
|
|
|
59
|
-
|
|
60
|
-
{
|
|
61
|
-
config: this.stateManager.getConfig(),
|
|
62
|
-
isUsingTestStore: () => this.isUsingTestStore(),
|
|
63
|
-
isInitialized: () => this.isInitialized(),
|
|
64
|
-
getCurrentUserId: () => this.stateManager.getCurrentUserId(),
|
|
65
|
-
setInitialized: (value) => this.stateManager.setInitialized(value),
|
|
66
|
-
setCurrentUserId: (id) => this.stateManager.setCurrentUserId(id),
|
|
67
|
-
},
|
|
68
|
-
userId,
|
|
69
|
-
apiKey
|
|
70
|
-
);
|
|
62
|
+
addPackageBreadcrumb("subscription", "Initialization started", { userId });
|
|
71
63
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
64
|
+
try {
|
|
65
|
+
const result = await initializeSDK(
|
|
66
|
+
{
|
|
67
|
+
config: this.stateManager.getConfig(),
|
|
68
|
+
isUsingTestStore: () => this.isUsingTestStore(),
|
|
69
|
+
isInitialized: () => this.isInitialized(),
|
|
70
|
+
getCurrentUserId: () => this.stateManager.getCurrentUserId(),
|
|
71
|
+
setInitialized: (value) => this.stateManager.setInitialized(value),
|
|
72
|
+
setCurrentUserId: (id) => this.stateManager.setCurrentUserId(id),
|
|
73
|
+
},
|
|
74
|
+
userId,
|
|
75
|
+
apiKey
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
if (result.success) {
|
|
79
|
+
this.listenerManager.setUserId(userId);
|
|
80
|
+
this.listenerManager.setupListener(this.stateManager.getConfig());
|
|
81
|
+
addPackageBreadcrumb("subscription", "Initialization successful", { userId });
|
|
82
|
+
} else {
|
|
83
|
+
trackPackageWarning("subscription", "Initialization failed", {
|
|
84
|
+
userId,
|
|
85
|
+
hasOffering: !!result.offering,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
76
88
|
|
|
77
|
-
|
|
89
|
+
return result;
|
|
90
|
+
} catch (error) {
|
|
91
|
+
trackPackageError(error instanceof Error ? error : new Error(String(error)), {
|
|
92
|
+
packageName: "subscription",
|
|
93
|
+
operation: "initialize",
|
|
94
|
+
userId,
|
|
95
|
+
});
|
|
96
|
+
throw error;
|
|
97
|
+
}
|
|
78
98
|
}
|
|
79
99
|
|
|
80
100
|
async fetchOfferings(): Promise<PurchasesOffering | null> {
|
|
@@ -113,13 +133,20 @@ export class RevenueCatService implements IRevenueCatService {
|
|
|
113
133
|
async reset(): Promise<void> {
|
|
114
134
|
if (!this.isInitialized()) return;
|
|
115
135
|
|
|
136
|
+
addPackageBreadcrumb("subscription", "Reset started", {
|
|
137
|
+
userId: this.getCurrentUserId(),
|
|
138
|
+
});
|
|
139
|
+
|
|
116
140
|
this.listenerManager.destroy();
|
|
117
141
|
|
|
118
142
|
try {
|
|
119
143
|
await Purchases.logOut();
|
|
120
144
|
this.stateManager.setInitialized(false);
|
|
121
|
-
|
|
122
|
-
|
|
145
|
+
addPackageBreadcrumb("subscription", "Reset successful", {});
|
|
146
|
+
} catch (error) {
|
|
147
|
+
trackPackageWarning("subscription", "Reset failed (non-critical)", {
|
|
148
|
+
error: error instanceof Error ? error.message : String(error),
|
|
149
|
+
});
|
|
123
150
|
}
|
|
124
151
|
}
|
|
125
152
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Initialization Cache
|
|
3
|
+
* Manages promise caching and user state for initialization
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { addPackageBreadcrumb } from "@umituz/react-native-sentry";
|
|
7
|
+
|
|
8
|
+
export class InitializationCache {
|
|
9
|
+
private initPromise: Promise<boolean> | null = null;
|
|
10
|
+
private currentUserId: string | null = null;
|
|
11
|
+
|
|
12
|
+
shouldReinitialize(userId: string): boolean {
|
|
13
|
+
if (!this.initPromise) {
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (this.currentUserId !== userId) {
|
|
18
|
+
addPackageBreadcrumb("subscription", "User changed, reinitialize needed", {
|
|
19
|
+
oldUserId: this.currentUserId,
|
|
20
|
+
newUserId: userId,
|
|
21
|
+
});
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
getExistingPromise(): Promise<boolean> | null {
|
|
29
|
+
return this.initPromise;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
setPromise(promise: Promise<boolean>, userId: string): void {
|
|
33
|
+
this.initPromise = promise;
|
|
34
|
+
this.currentUserId = userId;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
getCurrentUserId(): string | null {
|
|
38
|
+
return this.currentUserId;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
reset(): void {
|
|
42
|
+
this.initPromise = null;
|
|
43
|
+
this.currentUserId = null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User ID Provider
|
|
3
|
+
* Manages user ID retrieval (anonymous or authenticated)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
trackPackageError,
|
|
8
|
+
addPackageBreadcrumb,
|
|
9
|
+
} from "@umituz/react-native-sentry";
|
|
10
|
+
|
|
11
|
+
export class UserIdProvider {
|
|
12
|
+
private cachedAnonUserId: string | null = null;
|
|
13
|
+
private getAnonymousUserIdFn: (() => Promise<string>) | null = null;
|
|
14
|
+
|
|
15
|
+
configure(getAnonymousUserId: () => Promise<string>): void {
|
|
16
|
+
this.getAnonymousUserIdFn = getAnonymousUserId;
|
|
17
|
+
addPackageBreadcrumb("subscription", "UserIdProvider configured", {});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async getOrCreateAnonymousUserId(): Promise<string> {
|
|
21
|
+
if (this.cachedAnonUserId) {
|
|
22
|
+
return this.cachedAnonUserId;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (!this.getAnonymousUserIdFn) {
|
|
26
|
+
const error = new Error("Anonymous user ID provider not configured");
|
|
27
|
+
trackPackageError(error, {
|
|
28
|
+
packageName: "subscription",
|
|
29
|
+
operation: "get_anon_user_id",
|
|
30
|
+
});
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
this.cachedAnonUserId = await this.getAnonymousUserIdFn();
|
|
35
|
+
addPackageBreadcrumb("subscription", "Anonymous user ID created", {
|
|
36
|
+
userId: this.cachedAnonUserId,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
return this.cachedAnonUserId;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
reset(): void {
|
|
43
|
+
this.cachedAnonUserId = null;
|
|
44
|
+
}
|
|
45
|
+
}
|