@umituz/react-native-subscription 2.27.113 → 2.27.115
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 +27 -116
- package/src/domains/credits/application/credit-strategies/CreditAllocationOrchestrator.ts +1 -6
- package/src/domains/credits/application/creditDocumentHelpers.ts +58 -0
- package/src/domains/credits/application/creditOperationUtils.ts +154 -0
- package/src/domains/credits/presentation/useCredits.ts +1 -2
- package/src/domains/paywall/hooks/usePaywallActions.ts +0 -3
- package/src/domains/subscription/application/SubscriptionSyncService.ts +19 -20
- package/src/domains/subscription/core/RevenueCatError.ts +40 -31
- package/src/domains/subscription/infrastructure/handlers/PackageHandler.ts +0 -1
- package/src/domains/subscription/infrastructure/hooks/useRevenueCatTrialEligibility.ts +19 -85
- package/src/domains/subscription/infrastructure/managers/SubscriptionManager.ts +33 -75
- package/src/domains/subscription/infrastructure/managers/subscriptionManagerUtils.ts +57 -0
- package/src/domains/subscription/infrastructure/services/CustomerInfoListenerManager.ts +6 -12
- package/src/domains/subscription/infrastructure/services/PurchaseHandler.ts +0 -2
- package/src/domains/subscription/infrastructure/services/RevenueCatInitializer.ts +3 -4
- package/src/domains/subscription/infrastructure/services/RevenueCatService.ts +2 -5
- package/src/domains/subscription/infrastructure/utils/PremiumStatusSyncer.ts +6 -12
- package/src/domains/subscription/infrastructure/utils/authPurchaseState.ts +69 -0
- package/src/domains/subscription/infrastructure/utils/trialEligibilityUtils.ts +77 -0
- package/src/domains/subscription/presentation/components/feedback/FeedbackOption.tsx +139 -0
- package/src/domains/subscription/presentation/components/feedback/PaywallFeedbackModal.tsx +15 -70
- package/src/domains/subscription/presentation/components/feedback/paywallFeedbackStyles.ts +0 -92
- package/src/domains/subscription/presentation/stores/purchaseLoadingStore.ts +1 -18
- package/src/domains/subscription/presentation/useAuthAwarePurchase.ts +22 -76
- package/src/domains/subscription/presentation/usePremium.ts +2 -11
- package/src/domains/subscription/presentation/useSubscriptionStatus.ts +1 -6
- package/src/domains/trial/application/TrialService.ts +4 -8
- package/src/domains/wallet/infrastructure/services/ProductMetadataService.ts +0 -13
- package/src/domains/wallet/presentation/hooks/useProductMetadata.ts +0 -10
- package/src/domains/wallet/presentation/hooks/useTransactionHistory.ts +0 -8
- package/src/init/createSubscriptionInitModule.ts +1 -4
- package/src/presentation/hooks/feedback/useFeedbackSubmit.ts +0 -14
- package/src/shared/application/FeedbackService.ts +0 -21
- package/src/shared/infrastructure/SubscriptionEventBus.ts +2 -2
- package/src/shared/types/CommonTypes.ts +65 -0
- package/src/shared/utils/BaseError.ts +26 -0
- package/src/shared/utils/Logger.ts +14 -45
- package/src/shared/utils/SubscriptionError.ts +20 -30
- package/src/utils/packageTypeDetector.ts +0 -4
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.115",
|
|
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,16 @@
|
|
|
1
|
-
import {
|
|
2
|
-
getFirestore,
|
|
3
|
-
} from "@umituz/react-native-firebase";
|
|
4
|
-
import {
|
|
5
|
-
runTransaction,
|
|
6
|
-
serverTimestamp,
|
|
7
|
-
type Transaction,
|
|
8
|
-
type DocumentReference,
|
|
9
|
-
} from "firebase/firestore";
|
|
10
1
|
import type { CreditsConfig } from "../core/Credits";
|
|
11
2
|
import type { UserCreditsDocumentRead } from "../core/UserCreditsDocument";
|
|
12
|
-
import {
|
|
3
|
+
import { getAppVersion, validatePlatform } from "../../../utils";
|
|
4
|
+
import type { InitializeCreditsMetadata, InitializationResult } from "../../subscription/application/SubscriptionInitializerTypes";
|
|
5
|
+
import { runTransaction, type Transaction, type DocumentReference } from "firebase/firestore";
|
|
6
|
+
import type { Firestore } from "firebase/firestore";
|
|
7
|
+
import { getCreditDocumentOrDefault } from "./creditDocumentHelpers";
|
|
8
|
+
import { calculateNewCredits, buildCreditsData, shouldSkipStatusSyncWrite } from "./creditOperationUtils";
|
|
13
9
|
import { CreditLimitCalculator } from "./CreditLimitCalculator";
|
|
14
10
|
import { PurchaseMetadataGenerator } from "./PurchaseMetadataGenerator";
|
|
15
|
-
import { creditAllocationOrchestrator } from "./credit-strategies/CreditAllocationOrchestrator";
|
|
16
|
-
import { getAppVersion, validatePlatform, isPast } from "../../../utils";
|
|
17
|
-
import type { InitializeCreditsMetadata, InitializationResult } from "../../subscription/application/SubscriptionInitializerTypes";
|
|
18
11
|
|
|
19
12
|
export async function initializeCreditsTransaction(
|
|
20
|
-
db:
|
|
13
|
+
db: Firestore,
|
|
21
14
|
creditsRef: DocumentReference,
|
|
22
15
|
config: CreditsConfig,
|
|
23
16
|
purchaseId: string,
|
|
@@ -29,33 +22,9 @@ export async function initializeCreditsTransaction(
|
|
|
29
22
|
|
|
30
23
|
return runTransaction(db, async (transaction: Transaction) => {
|
|
31
24
|
const creditsDoc = await transaction.get(creditsRef);
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
: {
|
|
36
|
-
credits: 0,
|
|
37
|
-
creditLimit: 0,
|
|
38
|
-
isPremium: false,
|
|
39
|
-
status: "none",
|
|
40
|
-
processedPurchases: [],
|
|
41
|
-
purchaseHistory: [],
|
|
42
|
-
platform: validatePlatform() as any,
|
|
43
|
-
lastUpdatedAt: now,
|
|
44
|
-
purchasedAt: now,
|
|
45
|
-
expirationDate: null,
|
|
46
|
-
lastPurchaseAt: null,
|
|
47
|
-
willRenew: false,
|
|
48
|
-
productId: null,
|
|
49
|
-
packageType: null,
|
|
50
|
-
originalTransactionId: null,
|
|
51
|
-
appVersion: null,
|
|
52
|
-
periodType: null,
|
|
53
|
-
isTrialing: false,
|
|
54
|
-
trialStartDate: null,
|
|
55
|
-
trialEndDate: null,
|
|
56
|
-
trialCredits: 0,
|
|
57
|
-
convertedFromTrial: false,
|
|
58
|
-
} as any;
|
|
25
|
+
const platform = validatePlatform();
|
|
26
|
+
|
|
27
|
+
const existingData = getCreditDocumentOrDefault(creditsDoc, platform);
|
|
59
28
|
|
|
60
29
|
if (existingData.processedPurchases.includes(purchaseId)) {
|
|
61
30
|
return {
|
|
@@ -66,8 +35,6 @@ export async function initializeCreditsTransaction(
|
|
|
66
35
|
}
|
|
67
36
|
|
|
68
37
|
const creditLimit = CreditLimitCalculator.calculate(metadata.productId, config);
|
|
69
|
-
|
|
70
|
-
const platform = validatePlatform();
|
|
71
38
|
const appVersion = getAppVersion();
|
|
72
39
|
|
|
73
40
|
const { purchaseHistory } = PurchaseMetadataGenerator.generate({
|
|
@@ -79,85 +46,29 @@ export async function initializeCreditsTransaction(
|
|
|
79
46
|
appVersion,
|
|
80
47
|
}, existingData);
|
|
81
48
|
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
let isExpired = false;
|
|
85
|
-
if (metadata.expirationDate) {
|
|
86
|
-
isExpired = isPast(metadata.expirationDate);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const status = resolveSubscriptionStatus({
|
|
90
|
-
isPremium,
|
|
91
|
-
willRenew: metadata.willRenew ?? false,
|
|
92
|
-
isExpired,
|
|
93
|
-
periodType: metadata.periodType ?? undefined,
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
const isStatusSync = purchaseId.startsWith("status_sync_");
|
|
97
|
-
const isSubscriptionActive = isPremium && !isExpired;
|
|
98
|
-
|
|
99
|
-
const newCredits = creditAllocationOrchestrator.allocate({
|
|
100
|
-
status,
|
|
101
|
-
isStatusSync,
|
|
49
|
+
const newCredits = calculateNewCredits({
|
|
50
|
+
metadata,
|
|
102
51
|
existingData,
|
|
103
52
|
creditLimit,
|
|
104
|
-
|
|
105
|
-
productId: metadata.productId,
|
|
53
|
+
purchaseId,
|
|
106
54
|
});
|
|
107
55
|
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
isPremium,
|
|
112
|
-
status,
|
|
113
|
-
credits: newCredits,
|
|
56
|
+
const creditsData = buildCreditsData({
|
|
57
|
+
existingData,
|
|
58
|
+
newCredits,
|
|
114
59
|
creditLimit,
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
creditsData.purchaseHistory = purchaseHistory;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
const isNewPurchaseOrRenewal = purchaseId.startsWith("purchase_")
|
|
124
|
-
|| purchaseId.startsWith("renewal_");
|
|
125
|
-
|
|
126
|
-
if (isNewPurchaseOrRenewal) {
|
|
127
|
-
creditsData.lastPurchaseAt = now;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
if (metadata.expirationDate) {
|
|
131
|
-
creditsData.expirationDate = serverTimestamp();
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
if (metadata.willRenew !== undefined) {
|
|
135
|
-
creditsData.willRenew = metadata.willRenew;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
if (metadata.originalTransactionId) {
|
|
139
|
-
creditsData.originalTransactionId = metadata.originalTransactionId;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
creditsData.productId = metadata.productId;
|
|
143
|
-
creditsData.platform = platform;
|
|
144
|
-
|
|
145
|
-
// Skip write if it's a status sync and data hasn't changed to save costs
|
|
146
|
-
if (isStatusSync && existingData) {
|
|
147
|
-
const hasChanged =
|
|
148
|
-
existingData.isPremium !== creditsData.isPremium ||
|
|
149
|
-
existingData.status !== creditsData.status ||
|
|
150
|
-
existingData.credits !== creditsData.credits ||
|
|
151
|
-
existingData.creditLimit !== creditsData.creditLimit ||
|
|
152
|
-
existingData.productId !== creditsData.productId;
|
|
60
|
+
purchaseId,
|
|
61
|
+
metadata,
|
|
62
|
+
purchaseHistory,
|
|
63
|
+
platform,
|
|
64
|
+
});
|
|
153
65
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
}
|
|
66
|
+
if (shouldSkipStatusSyncWrite(purchaseId, existingData, creditsData)) {
|
|
67
|
+
return {
|
|
68
|
+
credits: existingData.credits,
|
|
69
|
+
alreadyProcessed: true,
|
|
70
|
+
finalData: existingData
|
|
71
|
+
};
|
|
161
72
|
}
|
|
162
73
|
|
|
163
74
|
transaction.set(creditsRef, creditsData, { merge: true });
|
|
@@ -18,16 +18,11 @@ export class CreditAllocationOrchestrator {
|
|
|
18
18
|
*/
|
|
19
19
|
allocate(params: CreditAllocationParams): number {
|
|
20
20
|
const strategy = this.strategies.find(s => s.canHandle(params));
|
|
21
|
-
|
|
21
|
+
|
|
22
22
|
if (!strategy) {
|
|
23
|
-
// Should theoretically never happen due to StandardPurchaseCreditStrategy fallback
|
|
24
23
|
return params.creditLimit;
|
|
25
24
|
}
|
|
26
25
|
|
|
27
|
-
if (__DEV__) {
|
|
28
|
-
console.log(`[CreditAllocationOrchestrator] Using strategy: ${strategy.constructor.name}`);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
26
|
return strategy.execute(params);
|
|
32
27
|
}
|
|
33
28
|
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Credit Document Helpers
|
|
3
|
+
* Utilities for getting and creating credit documents
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { UserCreditsDocumentRead } from "../core/UserCreditsDocument";
|
|
7
|
+
import { serverTimestamp, type DocumentSnapshot } from "firebase/firestore";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Get existing credit document or create default
|
|
11
|
+
*/
|
|
12
|
+
export function getCreditDocumentOrDefault(
|
|
13
|
+
creditsDoc: DocumentSnapshot,
|
|
14
|
+
platform: "ios" | "android"
|
|
15
|
+
): UserCreditsDocumentRead {
|
|
16
|
+
if (creditsDoc.exists()) {
|
|
17
|
+
return creditsDoc.data() as UserCreditsDocumentRead;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const now = serverTimestamp();
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
credits: 0,
|
|
24
|
+
creditLimit: 0,
|
|
25
|
+
isPremium: false,
|
|
26
|
+
status: "none",
|
|
27
|
+
processedPurchases: [],
|
|
28
|
+
purchaseHistory: [],
|
|
29
|
+
platform,
|
|
30
|
+
lastUpdatedAt: now,
|
|
31
|
+
purchasedAt: now,
|
|
32
|
+
expirationDate: null,
|
|
33
|
+
lastPurchaseAt: null,
|
|
34
|
+
willRenew: false,
|
|
35
|
+
productId: null,
|
|
36
|
+
packageType: null,
|
|
37
|
+
originalTransactionId: null,
|
|
38
|
+
appVersion: null,
|
|
39
|
+
periodType: null,
|
|
40
|
+
isTrialing: false,
|
|
41
|
+
trialStartDate: null,
|
|
42
|
+
trialEndDate: null,
|
|
43
|
+
trialCredits: 0,
|
|
44
|
+
convertedFromTrial: false,
|
|
45
|
+
} as any;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Add purchase ID to processed purchases list
|
|
50
|
+
* Maintains last 50 purchases
|
|
51
|
+
*/
|
|
52
|
+
export function addProcessedPurchase(
|
|
53
|
+
existing: string[],
|
|
54
|
+
purchaseId: string,
|
|
55
|
+
limit: number = 50
|
|
56
|
+
): string[] {
|
|
57
|
+
return [...existing, purchaseId].slice(-limit);
|
|
58
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Credit Operation Utilities
|
|
3
|
+
* Business logic for credit calculations and data building
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { resolveSubscriptionStatus } from "../../subscription/core/SubscriptionStatus";
|
|
7
|
+
import { creditAllocationOrchestrator } from "./credit-strategies/CreditAllocationOrchestrator";
|
|
8
|
+
import { isPast } from "../../../utils";
|
|
9
|
+
import type { UserCreditsDocumentRead } from "../core/UserCreditsDocument";
|
|
10
|
+
import type { InitializeCreditsMetadata } from "../../subscription/application/SubscriptionInitializerTypes";
|
|
11
|
+
import { serverTimestamp } from "firebase/firestore";
|
|
12
|
+
|
|
13
|
+
interface CalculateCreditsParams {
|
|
14
|
+
metadata: InitializeCreditsMetadata;
|
|
15
|
+
existingData: UserCreditsDocumentRead;
|
|
16
|
+
creditLimit: number;
|
|
17
|
+
purchaseId: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface BuildCreditsDataParams {
|
|
21
|
+
existingData: UserCreditsDocumentRead;
|
|
22
|
+
newCredits: number;
|
|
23
|
+
creditLimit: number;
|
|
24
|
+
purchaseId: string;
|
|
25
|
+
metadata: InitializeCreditsMetadata;
|
|
26
|
+
purchaseHistory: any[];
|
|
27
|
+
platform: "ios" | "android";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Calculate new credits based on subscription status
|
|
32
|
+
*/
|
|
33
|
+
export function calculateNewCredits(params: CalculateCreditsParams): number {
|
|
34
|
+
const { metadata, existingData, creditLimit, purchaseId } = params;
|
|
35
|
+
|
|
36
|
+
const isPremium = metadata.isPremium;
|
|
37
|
+
const isExpired = metadata.expirationDate ? isPast(metadata.expirationDate) : false;
|
|
38
|
+
|
|
39
|
+
const status = resolveSubscriptionStatus({
|
|
40
|
+
isPremium,
|
|
41
|
+
willRenew: metadata.willRenew ?? false,
|
|
42
|
+
isExpired,
|
|
43
|
+
periodType: metadata.periodType ?? undefined,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const isStatusSync = purchaseId.startsWith("status_sync_");
|
|
47
|
+
const isSubscriptionActive = isPremium && !isExpired;
|
|
48
|
+
|
|
49
|
+
return creditAllocationOrchestrator.allocate({
|
|
50
|
+
status,
|
|
51
|
+
isStatusSync,
|
|
52
|
+
existingData,
|
|
53
|
+
creditLimit,
|
|
54
|
+
isSubscriptionActive,
|
|
55
|
+
productId: metadata.productId,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Build credits data object for Firestore update
|
|
61
|
+
*/
|
|
62
|
+
export function buildCreditsData(params: BuildCreditsDataParams): Record<string, any> {
|
|
63
|
+
const {
|
|
64
|
+
existingData,
|
|
65
|
+
newCredits,
|
|
66
|
+
creditLimit,
|
|
67
|
+
purchaseId,
|
|
68
|
+
metadata,
|
|
69
|
+
purchaseHistory,
|
|
70
|
+
platform,
|
|
71
|
+
} = params;
|
|
72
|
+
|
|
73
|
+
const isPremium = metadata.isPremium;
|
|
74
|
+
const isExpired = metadata.expirationDate ? isPast(metadata.expirationDate) : false;
|
|
75
|
+
|
|
76
|
+
const status = resolveSubscriptionStatus({
|
|
77
|
+
isPremium,
|
|
78
|
+
willRenew: metadata.willRenew ?? false,
|
|
79
|
+
isExpired,
|
|
80
|
+
periodType: metadata.periodType ?? undefined,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const newProcessedPurchases = addProcessedPurchase(existingData.processedPurchases, purchaseId);
|
|
84
|
+
|
|
85
|
+
const creditsData: Record<string, any> = {
|
|
86
|
+
isPremium,
|
|
87
|
+
status,
|
|
88
|
+
credits: newCredits,
|
|
89
|
+
creditLimit,
|
|
90
|
+
lastUpdatedAt: serverTimestamp(),
|
|
91
|
+
processedPurchases: newProcessedPurchases,
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
if (purchaseHistory.length > 0) {
|
|
95
|
+
creditsData.purchaseHistory = purchaseHistory;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const isNewPurchaseOrRenewal = purchaseId.startsWith("purchase_") || purchaseId.startsWith("renewal_");
|
|
99
|
+
if (isNewPurchaseOrRenewal) {
|
|
100
|
+
creditsData.lastPurchaseAt = serverTimestamp();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (metadata.expirationDate) {
|
|
104
|
+
creditsData.expirationDate = serverTimestamp();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (metadata.willRenew !== undefined) {
|
|
108
|
+
creditsData.willRenew = metadata.willRenew;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (metadata.originalTransactionId) {
|
|
112
|
+
creditsData.originalTransactionId = metadata.originalTransactionId;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
creditsData.productId = metadata.productId;
|
|
116
|
+
creditsData.platform = platform;
|
|
117
|
+
|
|
118
|
+
return creditsData;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Check if status sync write should be skipped (no changes)
|
|
123
|
+
*/
|
|
124
|
+
export function shouldSkipStatusSyncWrite(
|
|
125
|
+
purchaseId: string,
|
|
126
|
+
existingData: UserCreditsDocumentRead,
|
|
127
|
+
newCreditsData: Record<string, any>
|
|
128
|
+
): boolean {
|
|
129
|
+
const isStatusSync = purchaseId.startsWith("status_sync_");
|
|
130
|
+
|
|
131
|
+
if (!isStatusSync) {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const hasChanged =
|
|
136
|
+
existingData.isPremium !== newCreditsData.isPremium ||
|
|
137
|
+
existingData.status !== newCreditsData.status ||
|
|
138
|
+
existingData.credits !== newCreditsData.credits ||
|
|
139
|
+
existingData.creditLimit !== newCreditsData.creditLimit ||
|
|
140
|
+
existingData.productId !== newCreditsData.productId;
|
|
141
|
+
|
|
142
|
+
return !hasChanged;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Add purchase ID to processed purchases list
|
|
147
|
+
*/
|
|
148
|
+
function addProcessedPurchase(
|
|
149
|
+
existing: string[],
|
|
150
|
+
purchaseId: string,
|
|
151
|
+
limit: number = 50
|
|
152
|
+
): string[] {
|
|
153
|
+
return [...existing, purchaseId].slice(-limit);
|
|
154
|
+
}
|
|
@@ -81,10 +81,9 @@ export const useCredits = (): UseCreditsResult => {
|
|
|
81
81
|
// Observer Pattern: Listen for credit updates
|
|
82
82
|
useEffect(() => {
|
|
83
83
|
if (!userId) return;
|
|
84
|
-
|
|
84
|
+
|
|
85
85
|
const unsubscribe = subscriptionEventBus.on(SUBSCRIPTION_EVENTS.CREDITS_UPDATED, (updatedUserId) => {
|
|
86
86
|
if (updatedUserId === userId) {
|
|
87
|
-
if (__DEV__) console.log("[useCredits] Event received: CREDITS_UPDATED, refetching...");
|
|
88
87
|
queryClient.invalidateQueries({ queryKey: creditsQueryKeys.user(userId) });
|
|
89
88
|
}
|
|
90
89
|
});
|
|
@@ -60,9 +60,6 @@ export function usePaywallActions({
|
|
|
60
60
|
} catch (error) {
|
|
61
61
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
62
62
|
onPurchaseError?.(err);
|
|
63
|
-
if (__DEV__) {
|
|
64
|
-
console.error("[usePaywallActions] Purchase failed:", err);
|
|
65
|
-
}
|
|
66
63
|
} finally {
|
|
67
64
|
setIsLocalProcessing(false);
|
|
68
65
|
endPurchase();
|
|
@@ -15,24 +15,23 @@ export class SubscriptionSyncService {
|
|
|
15
15
|
async handlePurchase(userId: string, productId: string, customerInfo: CustomerInfo, source?: PurchaseSource) {
|
|
16
16
|
try {
|
|
17
17
|
const revenueCatData = extractRevenueCatData(customerInfo, this.entitlementId);
|
|
18
|
-
const purchaseId = revenueCatData.originalTransactionId
|
|
18
|
+
const purchaseId = revenueCatData.originalTransactionId
|
|
19
19
|
? `purchase_${revenueCatData.originalTransactionId}`
|
|
20
20
|
: `purchase_${productId}_${Date.now()}`;
|
|
21
21
|
|
|
22
22
|
await getCreditsRepository().initializeCredits(
|
|
23
|
-
userId,
|
|
24
|
-
purchaseId,
|
|
25
|
-
productId,
|
|
26
|
-
source ?? PURCHASE_SOURCE.SETTINGS,
|
|
23
|
+
userId,
|
|
24
|
+
purchaseId,
|
|
25
|
+
productId,
|
|
26
|
+
source ?? PURCHASE_SOURCE.SETTINGS,
|
|
27
27
|
revenueCatData,
|
|
28
|
-
PURCHASE_TYPE.INITIAL
|
|
28
|
+
PURCHASE_TYPE.INITIAL
|
|
29
29
|
);
|
|
30
|
-
|
|
31
|
-
// Notify listeners via Event Bus
|
|
30
|
+
|
|
32
31
|
subscriptionEventBus.emit(SUBSCRIPTION_EVENTS.CREDITS_UPDATED, userId);
|
|
33
32
|
subscriptionEventBus.emit(SUBSCRIPTION_EVENTS.PURCHASE_COMPLETED, { userId, productId });
|
|
34
|
-
} catch
|
|
35
|
-
|
|
33
|
+
} catch {
|
|
34
|
+
// Ignored to prevent background sync errors from disrupting user experience
|
|
36
35
|
}
|
|
37
36
|
}
|
|
38
37
|
|
|
@@ -40,23 +39,23 @@ export class SubscriptionSyncService {
|
|
|
40
39
|
try {
|
|
41
40
|
const revenueCatData = extractRevenueCatData(customerInfo, this.entitlementId);
|
|
42
41
|
revenueCatData.expirationDate = newExpirationDate || revenueCatData.expirationDate;
|
|
43
|
-
const purchaseId = revenueCatData.originalTransactionId
|
|
42
|
+
const purchaseId = revenueCatData.originalTransactionId
|
|
44
43
|
? `renewal_${revenueCatData.originalTransactionId}_${newExpirationDate}`
|
|
45
44
|
: `renewal_${productId}_${Date.now()}`;
|
|
46
45
|
|
|
47
46
|
await getCreditsRepository().initializeCredits(
|
|
48
|
-
userId,
|
|
49
|
-
purchaseId,
|
|
50
|
-
productId,
|
|
51
|
-
PURCHASE_SOURCE.RENEWAL,
|
|
47
|
+
userId,
|
|
48
|
+
purchaseId,
|
|
49
|
+
productId,
|
|
50
|
+
PURCHASE_SOURCE.RENEWAL,
|
|
52
51
|
revenueCatData,
|
|
53
52
|
PURCHASE_TYPE.RENEWAL
|
|
54
53
|
);
|
|
55
|
-
|
|
54
|
+
|
|
56
55
|
subscriptionEventBus.emit(SUBSCRIPTION_EVENTS.CREDITS_UPDATED, userId);
|
|
57
56
|
subscriptionEventBus.emit(SUBSCRIPTION_EVENTS.RENEWAL_DETECTED, { userId, productId });
|
|
58
|
-
} catch
|
|
59
|
-
|
|
57
|
+
} catch {
|
|
58
|
+
// Ignored to prevent background sync errors from disrupting user experience
|
|
60
59
|
}
|
|
61
60
|
}
|
|
62
61
|
|
|
@@ -123,8 +122,8 @@ export class SubscriptionSyncService {
|
|
|
123
122
|
|
|
124
123
|
subscriptionEventBus.emit(SUBSCRIPTION_EVENTS.CREDITS_UPDATED, userId);
|
|
125
124
|
subscriptionEventBus.emit(SUBSCRIPTION_EVENTS.PREMIUM_STATUS_CHANGED, { userId, isPremium });
|
|
126
|
-
} catch
|
|
127
|
-
|
|
125
|
+
} catch {
|
|
126
|
+
// Ignored to prevent background sync errors from disrupting user experience
|
|
128
127
|
}
|
|
129
128
|
}
|
|
130
129
|
}
|
|
@@ -3,54 +3,63 @@
|
|
|
3
3
|
* Domain-specific error types for RevenueCat operations
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
import { BaseError } from "../../../shared/utils/BaseError";
|
|
7
|
+
|
|
8
|
+
export class RevenueCatError extends BaseError {
|
|
9
|
+
constructor(message: string, code: string = 'REVENUE_CAT_ERROR', cause?: Error) {
|
|
10
|
+
super(message, code, cause);
|
|
11
|
+
this.name = "RevenueCatError";
|
|
12
|
+
}
|
|
11
13
|
}
|
|
12
14
|
|
|
13
15
|
export class RevenueCatInitializationError extends RevenueCatError {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
constructor(message = "RevenueCat service is not initialized", cause?: Error) {
|
|
17
|
+
super(message, 'REVENUE_CAT_NOT_INITIALIZED', cause);
|
|
18
|
+
this.name = "RevenueCatInitializationError";
|
|
19
|
+
}
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
export class RevenueCatConfigurationError extends RevenueCatError {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
constructor(message = "RevenueCat configuration is invalid", cause?: Error) {
|
|
24
|
+
super(message, 'REVENUE_CAT_CONFIGURATION_ERROR', cause);
|
|
25
|
+
this.name = "RevenueCatConfigurationError";
|
|
26
|
+
}
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
export class RevenueCatPurchaseError extends RevenueCatError {
|
|
28
|
-
|
|
30
|
+
public readonly productId: string | undefined;
|
|
31
|
+
|
|
32
|
+
constructor(message: string, productId?: string, cause?: Error) {
|
|
33
|
+
super(message, 'REVENUE_CAT_PURCHASE_ERROR', cause);
|
|
34
|
+
this.name = "RevenueCatPurchaseError";
|
|
35
|
+
this.productId = productId;
|
|
36
|
+
}
|
|
29
37
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
38
|
+
override toJSON() {
|
|
39
|
+
return {
|
|
40
|
+
...super.toJSON(),
|
|
41
|
+
productId: this.productId,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
35
44
|
}
|
|
36
45
|
|
|
37
46
|
export class RevenueCatRestoreError extends RevenueCatError {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
47
|
+
constructor(message = "Failed to restore purchases", cause?: Error) {
|
|
48
|
+
super(message, 'REVENUE_CAT_RESTORE_ERROR', cause);
|
|
49
|
+
this.name = "RevenueCatRestoreError";
|
|
50
|
+
}
|
|
42
51
|
}
|
|
43
52
|
|
|
44
53
|
export class RevenueCatNetworkError extends RevenueCatError {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
54
|
+
constructor(message = "Network error during RevenueCat operation", cause?: Error) {
|
|
55
|
+
super(message, 'REVENUE_CAT_NETWORK_ERROR', cause);
|
|
56
|
+
this.name = "RevenueCatNetworkError";
|
|
57
|
+
}
|
|
49
58
|
}
|
|
50
59
|
|
|
51
60
|
export class RevenueCatExpoGoError extends RevenueCatError {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
61
|
+
constructor(message = "RevenueCat is not available in Expo Go. Use a development build or test store.", cause?: Error) {
|
|
62
|
+
super(message, 'REVENUE_CAT_EXPO_GO_ERROR', cause);
|
|
63
|
+
this.name = "RevenueCatExpoGoError";
|
|
64
|
+
}
|
|
56
65
|
}
|