@umituz/react-native-subscription 2.27.112 → 2.27.114
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 +28 -125
- package/src/domains/credits/application/credit-strategies/{CreditAllocationContext.ts → CreditAllocationOrchestrator.ts} +4 -9
- package/src/domains/credits/application/creditDocumentHelpers.ts +58 -0
- package/src/domains/credits/application/creditOperationUtils.ts +154 -0
- package/src/domains/credits/core/CreditsMapper.ts +8 -13
- package/src/domains/credits/infrastructure/{CreditsRepositoryProvider.ts → CreditsRepositoryManager.ts} +2 -2
- package/src/domains/credits/presentation/useCredits.ts +2 -3
- package/src/domains/credits/presentation/useDeductCredit.ts +4 -4
- package/src/domains/paywall/components/PaywallContainer.types.ts +1 -1
- package/src/domains/paywall/components/PaywallModal.tsx +28 -52
- package/src/domains/paywall/hooks/usePaywallActions.ts +77 -33
- package/src/domains/subscription/application/SubscriptionInitializer.ts +1 -1
- package/src/domains/subscription/application/SubscriptionSyncService.ts +17 -21
- 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 +3 -12
- package/src/domains/subscription/infrastructure/services/PurchaseHandler.ts +0 -2
- package/src/domains/subscription/infrastructure/services/RevenueCatInitializer.ts +2 -4
- package/src/domains/subscription/infrastructure/services/RevenueCatService.ts +1 -5
- package/src/domains/subscription/infrastructure/utils/PremiumStatusSyncer.ts +3 -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/screens/SubscriptionDetailScreen.tsx +1 -1
- package/src/domains/subscription/presentation/stores/purchaseLoadingStore.ts +1 -18
- package/src/domains/subscription/presentation/useAuthAwarePurchase.ts +19 -69
- package/src/domains/subscription/presentation/usePaywallVisibility.ts +1 -1
- 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/index.ts +0 -6
- package/src/domains/wallet/infrastructure/repositories/TransactionRepository.ts +1 -1
- 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/domains/wallet/presentation/screens/WalletScreen.tsx +57 -43
- package/src/index.ts +1 -1
- package/src/init/createSubscriptionInitModule.ts +1 -4
- package/src/presentation/hooks/feedback/useFeedbackSubmit.ts +0 -14
- package/src/shared/application/ActivationHandler.ts +6 -6
- package/src/shared/application/FeedbackService.ts +0 -21
- package/src/shared/infrastructure/SubscriptionEventBus.ts +1 -2
- package/src/shared/presentation/index.ts +1 -0
- package/src/shared/presentation/layouts/ScreenLayout.tsx +79 -0
- package/src/shared/types/CommonTypes.ts +65 -0
- package/src/shared/utils/BaseError.ts +26 -0
- package/src/shared/utils/Logger.ts +15 -46
- package/src/shared/utils/Result.ts +16 -0
- package/src/shared/utils/SubscriptionConfig.ts +1 -1
- package/src/shared/utils/SubscriptionError.ts +20 -30
- package/src/utils/appUtils.ts +34 -0
- package/src/utils/dateUtils.ts +32 -0
- package/src/utils/index.ts +2 -0
- package/src/utils/packageTypeDetector.ts +0 -4
- package/src/domains/wallet/presentation/screens/WalletScreenContainer.tsx +0 -88
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.114",
|
|
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,24 +1,16 @@
|
|
|
1
|
-
import { Platform } from "react-native";
|
|
2
|
-
import Constants from "expo-constants";
|
|
3
|
-
import {
|
|
4
|
-
getFirestore,
|
|
5
|
-
} from "@umituz/react-native-firebase";
|
|
6
|
-
import {
|
|
7
|
-
runTransaction,
|
|
8
|
-
serverTimestamp,
|
|
9
|
-
type Transaction,
|
|
10
|
-
type DocumentReference,
|
|
11
|
-
} from "firebase/firestore";
|
|
12
1
|
import type { CreditsConfig } from "../core/Credits";
|
|
13
2
|
import type { UserCreditsDocumentRead } from "../core/UserCreditsDocument";
|
|
14
|
-
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";
|
|
15
9
|
import { CreditLimitCalculator } from "./CreditLimitCalculator";
|
|
16
10
|
import { PurchaseMetadataGenerator } from "./PurchaseMetadataGenerator";
|
|
17
|
-
import { creditAllocationContext } from "./credit-strategies/CreditAllocationContext";
|
|
18
|
-
import type { InitializeCreditsMetadata, InitializationResult } from "../../subscription/application/SubscriptionInitializerTypes";
|
|
19
11
|
|
|
20
12
|
export async function initializeCreditsTransaction(
|
|
21
|
-
db:
|
|
13
|
+
db: Firestore,
|
|
22
14
|
creditsRef: DocumentReference,
|
|
23
15
|
config: CreditsConfig,
|
|
24
16
|
purchaseId: string,
|
|
@@ -30,33 +22,9 @@ export async function initializeCreditsTransaction(
|
|
|
30
22
|
|
|
31
23
|
return runTransaction(db, async (transaction: Transaction) => {
|
|
32
24
|
const creditsDoc = await transaction.get(creditsRef);
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
: {
|
|
37
|
-
credits: 0,
|
|
38
|
-
creditLimit: 0,
|
|
39
|
-
isPremium: false,
|
|
40
|
-
status: "none",
|
|
41
|
-
processedPurchases: [],
|
|
42
|
-
purchaseHistory: [],
|
|
43
|
-
platform: Platform.OS as any,
|
|
44
|
-
lastUpdatedAt: now,
|
|
45
|
-
purchasedAt: now,
|
|
46
|
-
expirationDate: null,
|
|
47
|
-
lastPurchaseAt: null,
|
|
48
|
-
willRenew: false,
|
|
49
|
-
productId: null,
|
|
50
|
-
packageType: null,
|
|
51
|
-
originalTransactionId: null,
|
|
52
|
-
appVersion: null,
|
|
53
|
-
periodType: null,
|
|
54
|
-
isTrialing: false,
|
|
55
|
-
trialStartDate: null,
|
|
56
|
-
trialEndDate: null,
|
|
57
|
-
trialCredits: 0,
|
|
58
|
-
convertedFromTrial: false,
|
|
59
|
-
} as any;
|
|
25
|
+
const platform = validatePlatform();
|
|
26
|
+
|
|
27
|
+
const existingData = getCreditDocumentOrDefault(creditsDoc, platform);
|
|
60
28
|
|
|
61
29
|
if (existingData.processedPurchases.includes(purchaseId)) {
|
|
62
30
|
return {
|
|
@@ -67,16 +35,7 @@ export async function initializeCreditsTransaction(
|
|
|
67
35
|
}
|
|
68
36
|
|
|
69
37
|
const creditLimit = CreditLimitCalculator.calculate(metadata.productId, config);
|
|
70
|
-
|
|
71
|
-
const platform = Platform.OS;
|
|
72
|
-
if (platform !== "ios" && platform !== "android") {
|
|
73
|
-
throw new Error(`Invalid platform: ${platform}`);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const appVersion = Constants.expoConfig?.version;
|
|
77
|
-
if (!appVersion) {
|
|
78
|
-
throw new Error("appVersion is required in expoConfig");
|
|
79
|
-
}
|
|
38
|
+
const appVersion = getAppVersion();
|
|
80
39
|
|
|
81
40
|
const { purchaseHistory } = PurchaseMetadataGenerator.generate({
|
|
82
41
|
productId: metadata.productId,
|
|
@@ -87,85 +46,29 @@ export async function initializeCreditsTransaction(
|
|
|
87
46
|
appVersion,
|
|
88
47
|
}, existingData);
|
|
89
48
|
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
let isExpired = false;
|
|
93
|
-
if (metadata.expirationDate) {
|
|
94
|
-
isExpired = new Date(metadata.expirationDate).getTime() < Date.now();
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const status = resolveSubscriptionStatus({
|
|
98
|
-
isPremium,
|
|
99
|
-
willRenew: metadata.willRenew ?? false,
|
|
100
|
-
isExpired,
|
|
101
|
-
periodType: metadata.periodType ?? undefined,
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
const isStatusSync = purchaseId.startsWith("status_sync_");
|
|
105
|
-
const isSubscriptionActive = isPremium && !isExpired;
|
|
106
|
-
|
|
107
|
-
const newCredits = creditAllocationContext.allocate({
|
|
108
|
-
status,
|
|
109
|
-
isStatusSync,
|
|
49
|
+
const newCredits = calculateNewCredits({
|
|
50
|
+
metadata,
|
|
110
51
|
existingData,
|
|
111
52
|
creditLimit,
|
|
112
|
-
|
|
113
|
-
productId: metadata.productId,
|
|
53
|
+
purchaseId,
|
|
114
54
|
});
|
|
115
55
|
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
isPremium,
|
|
120
|
-
status,
|
|
121
|
-
credits: newCredits,
|
|
56
|
+
const creditsData = buildCreditsData({
|
|
57
|
+
existingData,
|
|
58
|
+
newCredits,
|
|
122
59
|
creditLimit,
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
creditsData.purchaseHistory = purchaseHistory;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
const isNewPurchaseOrRenewal = purchaseId.startsWith("purchase_")
|
|
132
|
-
|| purchaseId.startsWith("renewal_");
|
|
133
|
-
|
|
134
|
-
if (isNewPurchaseOrRenewal) {
|
|
135
|
-
creditsData.lastPurchaseAt = now;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
if (metadata.expirationDate) {
|
|
139
|
-
creditsData.expirationDate = serverTimestamp();
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
if (metadata.willRenew !== undefined) {
|
|
143
|
-
creditsData.willRenew = metadata.willRenew;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
if (metadata.originalTransactionId) {
|
|
147
|
-
creditsData.originalTransactionId = metadata.originalTransactionId;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
creditsData.productId = metadata.productId;
|
|
151
|
-
creditsData.platform = platform;
|
|
152
|
-
|
|
153
|
-
// Skip write if it's a status sync and data hasn't changed to save costs
|
|
154
|
-
if (isStatusSync && existingData) {
|
|
155
|
-
const hasChanged =
|
|
156
|
-
existingData.isPremium !== creditsData.isPremium ||
|
|
157
|
-
existingData.status !== creditsData.status ||
|
|
158
|
-
existingData.credits !== creditsData.credits ||
|
|
159
|
-
existingData.creditLimit !== creditsData.creditLimit ||
|
|
160
|
-
existingData.productId !== creditsData.productId;
|
|
60
|
+
purchaseId,
|
|
61
|
+
metadata,
|
|
62
|
+
purchaseHistory,
|
|
63
|
+
platform,
|
|
64
|
+
});
|
|
161
65
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
}
|
|
66
|
+
if (shouldSkipStatusSyncWrite(purchaseId, existingData, creditsData)) {
|
|
67
|
+
return {
|
|
68
|
+
credits: existingData.credits,
|
|
69
|
+
alreadyProcessed: true,
|
|
70
|
+
finalData: existingData
|
|
71
|
+
};
|
|
169
72
|
}
|
|
170
73
|
|
|
171
74
|
transaction.set(creditsRef, creditsData, { merge: true });
|
|
@@ -4,9 +4,9 @@ import { TrialCreditStrategy } from "./TrialCreditStrategy";
|
|
|
4
4
|
import { StandardPurchaseCreditStrategy } from "./StandardPurchaseCreditStrategy";
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
7
|
+
* Orchestrator to coordinate credit allocation logic using the Strategy Pattern.
|
|
8
8
|
*/
|
|
9
|
-
export class
|
|
9
|
+
export class CreditAllocationOrchestrator {
|
|
10
10
|
private strategies: ICreditStrategy[] = [
|
|
11
11
|
new SyncCreditStrategy(),
|
|
12
12
|
new TrialCreditStrategy(),
|
|
@@ -18,18 +18,13 @@ export class CreditAllocationContext {
|
|
|
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(`[CreditAllocationContext] Using strategy: ${strategy.constructor.name}`);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
26
|
return strategy.execute(params);
|
|
32
27
|
}
|
|
33
28
|
}
|
|
34
29
|
|
|
35
|
-
export const
|
|
30
|
+
export const creditAllocationOrchestrator = new CreditAllocationOrchestrator();
|
|
@@ -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
|
+
}
|
|
@@ -3,17 +3,12 @@ import { resolveSubscriptionStatus } from "../../subscription/core/SubscriptionS
|
|
|
3
3
|
import type { PeriodType, SubscriptionStatusType } from "../../subscription/core/SubscriptionConstants";
|
|
4
4
|
import type { UserCreditsDocumentRead } from "./UserCreditsDocument";
|
|
5
5
|
|
|
6
|
+
import { toSafeDate } from "../../../utils/dateUtils";
|
|
7
|
+
|
|
6
8
|
/** Maps Firestore document to domain entity with expiration validation */
|
|
7
9
|
export class CreditsMapper {
|
|
8
10
|
static toEntity(doc: UserCreditsDocumentRead): UserCredits {
|
|
9
|
-
const
|
|
10
|
-
if (!ts) return null;
|
|
11
|
-
if (typeof ts.toDate === "function") return ts.toDate();
|
|
12
|
-
if (ts instanceof Date) return ts;
|
|
13
|
-
return null;
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
const expirationDate = safeDate(doc.expirationDate);
|
|
11
|
+
const expirationDate = toSafeDate(doc.expirationDate);
|
|
17
12
|
const periodType = doc.periodType;
|
|
18
13
|
|
|
19
14
|
// Validate isPremium against expirationDate (real-time check)
|
|
@@ -25,10 +20,10 @@ export class CreditsMapper {
|
|
|
25
20
|
status,
|
|
26
21
|
|
|
27
22
|
// Dates
|
|
28
|
-
purchasedAt:
|
|
23
|
+
purchasedAt: toSafeDate(doc.purchasedAt) ?? new Date(),
|
|
29
24
|
expirationDate,
|
|
30
|
-
lastUpdatedAt:
|
|
31
|
-
lastPurchaseAt:
|
|
25
|
+
lastUpdatedAt: toSafeDate(doc.lastUpdatedAt) ?? new Date(),
|
|
26
|
+
lastPurchaseAt: toSafeDate(doc.lastPurchaseAt),
|
|
32
27
|
|
|
33
28
|
// RevenueCat details
|
|
34
29
|
willRenew: doc.willRenew,
|
|
@@ -39,8 +34,8 @@ export class CreditsMapper {
|
|
|
39
34
|
// Trial fields
|
|
40
35
|
periodType,
|
|
41
36
|
isTrialing: doc.isTrialing,
|
|
42
|
-
trialStartDate:
|
|
43
|
-
trialEndDate:
|
|
37
|
+
trialStartDate: toSafeDate(doc.trialStartDate),
|
|
38
|
+
trialEndDate: toSafeDate(doc.trialEndDate),
|
|
44
39
|
trialCredits: doc.trialCredits,
|
|
45
40
|
convertedFromTrial: doc.convertedFromTrial,
|
|
46
41
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Credits Repository
|
|
2
|
+
* Credits Repository Manager
|
|
3
3
|
* Module-level singleton for credits repository configuration
|
|
4
|
-
*
|
|
4
|
+
* Provides a clean, testable approach for repository access
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type { CreditsConfig } from "../core/Credits";
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
getCreditsRepository,
|
|
15
15
|
getCreditsConfig,
|
|
16
16
|
isCreditsRepositoryConfigured,
|
|
17
|
-
} from "../infrastructure/
|
|
17
|
+
} from "../infrastructure/CreditsRepositoryManager";
|
|
18
18
|
import { calculateCreditPercentage, canAffordCost } from "../utils/creditCalculations";
|
|
19
19
|
|
|
20
20
|
export const creditsQueryKeys = {
|
|
@@ -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
|
});
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import { useCallback } from "react";
|
|
7
7
|
import { useMutation, useQueryClient } from "@umituz/react-native-design-system";
|
|
8
8
|
import type { UserCredits } from "../core/Credits";
|
|
9
|
-
import { getCreditsRepository } from "../infrastructure/
|
|
9
|
+
import { getCreditsRepository } from "../infrastructure/CreditsRepositoryManager";
|
|
10
10
|
import { creditsQueryKeys } from "./useCredits";
|
|
11
11
|
import { calculateRemainingCredits } from "../utils/creditCalculations";
|
|
12
12
|
|
|
@@ -65,11 +65,11 @@ export const useDeductCredit = ({
|
|
|
65
65
|
wasInsufficient: previousCredits.credits < cost
|
|
66
66
|
};
|
|
67
67
|
},
|
|
68
|
-
onError: (_err, _cost,
|
|
68
|
+
onError: (_err, _cost, mutationData) => {
|
|
69
69
|
// Always restore previous credits on error to prevent UI desync
|
|
70
70
|
// Use optional chaining to be safe
|
|
71
|
-
if (userId &&
|
|
72
|
-
queryClient.setQueryData(creditsQueryKeys.user(userId),
|
|
71
|
+
if (userId && mutationData?.previousCredits && !mutationData.skippedOptimistic) {
|
|
72
|
+
queryClient.setQueryData(creditsQueryKeys.user(userId), mutationData.previousCredits);
|
|
73
73
|
}
|
|
74
74
|
},
|
|
75
75
|
onSuccess: () => {
|
|
@@ -45,7 +45,7 @@ export interface PaywallContainerProps {
|
|
|
45
45
|
/** Callback when purchase succeeds */
|
|
46
46
|
readonly onPurchaseSuccess?: () => void;
|
|
47
47
|
/** Callback when purchase fails */
|
|
48
|
-
readonly onPurchaseError?: (error: string) => void;
|
|
48
|
+
readonly onPurchaseError?: (error: Error | string) => void;
|
|
49
49
|
/** Callback when auth is required (for anonymous users) */
|
|
50
50
|
readonly onAuthRequired?: () => void;
|
|
51
51
|
/** Visibility override */
|