@umituz/react-native-subscription 2.3.8 → 2.5.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/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/ApiKeyResolver.ts +12 -12
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-subscription",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.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",
|
|
@@ -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
|
}
|
|
@@ -38,15 +38,16 @@ export function shouldUseTestStore(config: RevenueCatConfig): boolean {
|
|
|
38
38
|
export function resolveApiKey(config: RevenueCatConfig): string | null {
|
|
39
39
|
const useTestStore = shouldUseTestStore(config);
|
|
40
40
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
41
|
+
// Always log in development, log warnings in production for debugging
|
|
42
|
+
/* eslint-disable-next-line no-console */
|
|
43
|
+
console.log("[RevenueCat] resolveApiKey:", {
|
|
44
|
+
platform: Platform.OS,
|
|
45
|
+
useTestStore,
|
|
46
|
+
hasTestKey: !!config.testStoreKey,
|
|
47
|
+
hasIosKey: !!config.iosApiKey,
|
|
48
|
+
hasAndroidKey: !!config.androidApiKey,
|
|
49
|
+
isProduction: isProductionBuild(),
|
|
50
|
+
});
|
|
50
51
|
|
|
51
52
|
if (useTestStore) {
|
|
52
53
|
return config.testStoreKey ?? null;
|
|
@@ -59,9 +60,8 @@ export function resolveApiKey(config: RevenueCatConfig): string | null {
|
|
|
59
60
|
: config.iosApiKey;
|
|
60
61
|
|
|
61
62
|
if (!key || key === "" || key.includes("YOUR_")) {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}
|
|
63
|
+
/* eslint-disable-next-line no-console */
|
|
64
|
+
console.warn("[RevenueCat] ⚠️ NO API KEY - packages will not load. Check env vars.");
|
|
65
65
|
return null;
|
|
66
66
|
}
|
|
67
67
|
|