@umituz/react-native-subscription 2.27.92 → 2.27.94
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 +99 -40
- package/src/domains/credits/application/DeductCreditsCommand.ts +31 -13
- package/src/domains/credits/application/PurchaseMetadataGenerator.ts +17 -23
- package/src/domains/credits/core/Credits.ts +39 -39
- package/src/domains/credits/core/CreditsMapper.ts +11 -10
- package/src/domains/credits/core/UserCreditsDocument.ts +33 -33
- package/src/domains/credits/infrastructure/CreditsRepository.ts +46 -59
- package/src/domains/paywall/components/PaywallModal.tsx +1 -1
- package/src/domains/subscription/application/SubscriptionInitializer.ts +59 -18
- package/src/domains/subscription/application/SubscriptionInitializerTypes.ts +20 -20
- package/src/domains/subscription/infrastructure/handlers/PackageHandler.ts +46 -27
- package/src/domains/subscription/infrastructure/managers/SubscriptionManager.ts +106 -42
- package/src/domains/subscription/infrastructure/services/RestoreHandler.ts +4 -2
- package/src/domains/subscription/infrastructure/services/RevenueCatInitializer.ts +1 -2
- package/src/domains/subscription/infrastructure/utils/RenewalDetector.ts +1 -1
- package/src/domains/subscription/presentation/components/details/PremiumStatusBadge.tsx +6 -4
- package/src/domains/subscription/presentation/components/feedback/PaywallFeedbackModal.tsx +1 -1
- package/src/domains/subscription/presentation/types/SubscriptionDetailTypes.ts +4 -2
- package/src/domains/subscription/presentation/types/SubscriptionSettingsTypes.ts +1 -1
- package/src/domains/subscription/presentation/usePremiumGate.ts +1 -1
- package/src/domains/subscription/presentation/useSavedPurchaseAutoExecution.ts +1 -1
- package/src/domains/subscription/presentation/useSubscriptionSettingsConfig.ts +4 -3
- package/src/domains/subscription/presentation/useSubscriptionSettingsConfig.utils.ts +1 -1
- package/src/domains/trial/application/TrialEligibilityService.ts +1 -1
- package/src/domains/trial/infrastructure/DeviceTrialRepository.ts +2 -2
- package/src/shared/application/ports/IRevenueCatService.ts +2 -0
- package/src/shared/infrastructure/SubscriptionEventBus.ts +5 -2
- package/src/presentation/README.md +0 -125
- package/src/presentation/hooks/README.md +0 -156
- package/src/presentation/hooks/useAuthSubscriptionSync.md +0 -94
- package/src/presentation/hooks/useCredits.md +0 -103
- package/src/presentation/hooks/useDeductCredit.md +0 -100
- package/src/presentation/hooks/useFeatureGate.md +0 -112
- package/src/presentation/hooks/usePaywall.md +0 -89
- package/src/presentation/hooks/usePaywallOperations.md +0 -92
- package/src/presentation/hooks/usePaywallVisibility.md +0 -95
- package/src/presentation/hooks/usePremium.md +0 -88
- package/src/presentation/hooks/useSubscriptionSettingsConfig.md +0 -94
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.94",
|
|
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,13 +1,14 @@
|
|
|
1
1
|
import { Platform } from "react-native";
|
|
2
2
|
import Constants from "expo-constants";
|
|
3
|
+
import {
|
|
4
|
+
getFirestore,
|
|
5
|
+
} from "@umituz/react-native-firebase";
|
|
3
6
|
import {
|
|
4
7
|
runTransaction,
|
|
5
8
|
serverTimestamp,
|
|
6
|
-
Timestamp,
|
|
7
9
|
type Transaction,
|
|
8
10
|
type DocumentReference,
|
|
9
|
-
|
|
10
|
-
} from "@umituz/react-native-firebase";
|
|
11
|
+
} from "firebase/firestore";
|
|
11
12
|
import type { CreditsConfig } from "../core/Credits";
|
|
12
13
|
import type { UserCreditsDocumentRead } from "../core/UserCreditsDocument";
|
|
13
14
|
import { resolveSubscriptionStatus } from "../../subscription/core/SubscriptionStatus";
|
|
@@ -17,41 +18,72 @@ import { creditAllocationContext } from "./credit-strategies/CreditAllocationCon
|
|
|
17
18
|
import type { InitializeCreditsMetadata, InitializationResult } from "../../subscription/application/SubscriptionInitializerTypes";
|
|
18
19
|
|
|
19
20
|
export async function initializeCreditsTransaction(
|
|
20
|
-
db:
|
|
21
|
+
db: ReturnType<typeof getFirestore>,
|
|
21
22
|
creditsRef: DocumentReference,
|
|
22
23
|
config: CreditsConfig,
|
|
23
|
-
purchaseId
|
|
24
|
-
metadata
|
|
24
|
+
purchaseId: string,
|
|
25
|
+
metadata: InitializeCreditsMetadata
|
|
25
26
|
): Promise<InitializationResult> {
|
|
27
|
+
if (!db) {
|
|
28
|
+
throw new Error("Firestore instance is not available");
|
|
29
|
+
}
|
|
30
|
+
|
|
26
31
|
return runTransaction(db, async (transaction: Transaction) => {
|
|
27
32
|
const creditsDoc = await transaction.get(creditsRef);
|
|
28
33
|
const now = serverTimestamp();
|
|
29
|
-
const existingData = creditsDoc.exists()
|
|
34
|
+
const existingData = creditsDoc.exists()
|
|
35
|
+
? creditsDoc.data() as UserCreditsDocumentRead
|
|
36
|
+
: null;
|
|
37
|
+
|
|
38
|
+
if (!existingData) {
|
|
39
|
+
throw new Error("Credits document does not exist");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (existingData.processedPurchases.includes(purchaseId)) {
|
|
43
|
+
return {
|
|
44
|
+
credits: existingData.credits,
|
|
45
|
+
alreadyProcessed: true,
|
|
46
|
+
finalData: existingData
|
|
47
|
+
};
|
|
48
|
+
}
|
|
30
49
|
|
|
31
|
-
|
|
32
|
-
|
|
50
|
+
const creditLimit = CreditLimitCalculator.calculate(metadata.productId, config);
|
|
51
|
+
|
|
52
|
+
const platform = Platform.OS;
|
|
53
|
+
if (platform !== "ios" && platform !== "android") {
|
|
54
|
+
throw new Error(`Invalid platform: ${platform}`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const appVersion = Constants.expoConfig?.version;
|
|
58
|
+
if (!appVersion) {
|
|
59
|
+
throw new Error("appVersion is required in expoConfig");
|
|
33
60
|
}
|
|
34
61
|
|
|
35
|
-
const creditLimit = CreditLimitCalculator.calculate(metadata?.productId, config);
|
|
36
62
|
const { purchaseHistory } = PurchaseMetadataGenerator.generate({
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
63
|
+
productId: metadata.productId,
|
|
64
|
+
source: metadata.source,
|
|
65
|
+
type: metadata.type,
|
|
66
|
+
creditLimit,
|
|
67
|
+
platform,
|
|
68
|
+
appVersion,
|
|
43
69
|
}, existingData);
|
|
44
70
|
|
|
45
|
-
const isPremium = metadata
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
71
|
+
const isPremium = metadata.isPremium;
|
|
72
|
+
|
|
73
|
+
let isExpired = false;
|
|
74
|
+
if (metadata.expirationDate) {
|
|
75
|
+
isExpired = new Date(metadata.expirationDate).getTime() < Date.now();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const status = resolveSubscriptionStatus({
|
|
79
|
+
isPremium,
|
|
80
|
+
willRenew: metadata.willRenew ?? false,
|
|
81
|
+
isExpired,
|
|
82
|
+
periodType: metadata.periodType ?? undefined,
|
|
49
83
|
});
|
|
50
84
|
|
|
51
|
-
|
|
52
|
-
const isStatusSync = purchaseId?.startsWith("status_sync_") ?? false;
|
|
85
|
+
const isStatusSync = purchaseId.startsWith("status_sync_");
|
|
53
86
|
const isSubscriptionActive = isPremium && !isExpired;
|
|
54
|
-
const productId = metadata?.productId;
|
|
55
87
|
|
|
56
88
|
const newCredits = creditAllocationContext.allocate({
|
|
57
89
|
status,
|
|
@@ -59,30 +91,57 @@ export async function initializeCreditsTransaction(
|
|
|
59
91
|
existingData,
|
|
60
92
|
creditLimit,
|
|
61
93
|
isSubscriptionActive,
|
|
62
|
-
productId,
|
|
94
|
+
productId: metadata.productId,
|
|
63
95
|
});
|
|
64
96
|
|
|
97
|
+
const newProcessedPurchases = [...existingData.processedPurchases, purchaseId].slice(-50);
|
|
98
|
+
|
|
65
99
|
const creditsData: Record<string, any> = {
|
|
66
|
-
isPremium,
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
100
|
+
isPremium,
|
|
101
|
+
status,
|
|
102
|
+
credits: newCredits,
|
|
103
|
+
creditLimit,
|
|
104
|
+
lastUpdatedAt: now,
|
|
105
|
+
processedPurchases: newProcessedPurchases,
|
|
71
106
|
};
|
|
72
107
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
108
|
+
if (purchaseHistory.length > 0) {
|
|
109
|
+
creditsData.purchaseHistory = purchaseHistory;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const isNewPurchaseOrRenewal = purchaseId.startsWith("purchase_")
|
|
113
|
+
|| purchaseId.startsWith("renewal_");
|
|
114
|
+
|
|
115
|
+
if (isNewPurchaseOrRenewal) {
|
|
116
|
+
creditsData.lastPurchaseAt = now;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (metadata.expirationDate) {
|
|
120
|
+
creditsData.expirationDate = serverTimestamp();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (metadata.willRenew !== undefined) {
|
|
124
|
+
creditsData.willRenew = metadata.willRenew;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (metadata.originalTransactionId) {
|
|
128
|
+
creditsData.originalTransactionId = metadata.originalTransactionId;
|
|
81
129
|
}
|
|
82
130
|
|
|
131
|
+
creditsData.productId = metadata.productId;
|
|
132
|
+
creditsData.platform = platform;
|
|
133
|
+
|
|
83
134
|
transaction.set(creditsRef, creditsData, { merge: true });
|
|
84
|
-
|
|
85
|
-
const finalData = {
|
|
86
|
-
|
|
135
|
+
|
|
136
|
+
const finalData: UserCreditsDocumentRead = {
|
|
137
|
+
...existingData,
|
|
138
|
+
...creditsData,
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
credits: newCredits,
|
|
143
|
+
alreadyProcessed: false,
|
|
144
|
+
finalData
|
|
145
|
+
};
|
|
87
146
|
});
|
|
88
147
|
}
|
|
@@ -16,37 +16,55 @@ export class DeductCreditsCommand implements IDeductCreditsCommand {
|
|
|
16
16
|
private getCreditsRef: (db: Firestore, userId: string) => DocumentReference
|
|
17
17
|
) {}
|
|
18
18
|
|
|
19
|
-
async execute(userId: string, cost: number
|
|
19
|
+
async execute(userId: string, cost: number): Promise<DeductCreditsResult> {
|
|
20
20
|
const db = getFirestore();
|
|
21
|
-
if (!db)
|
|
21
|
+
if (!db) {
|
|
22
|
+
return {
|
|
23
|
+
success: false,
|
|
24
|
+
remainingCredits: null,
|
|
25
|
+
error: { message: "No DB", code: "ERR" }
|
|
26
|
+
};
|
|
27
|
+
}
|
|
22
28
|
|
|
23
29
|
try {
|
|
24
30
|
const remaining = await runTransaction(db, async (tx: Transaction) => {
|
|
25
31
|
const ref = this.getCreditsRef(db, userId);
|
|
26
32
|
const docSnap = await tx.get(ref);
|
|
27
|
-
|
|
28
|
-
if (!docSnap.exists())
|
|
29
|
-
|
|
33
|
+
|
|
34
|
+
if (!docSnap.exists()) {
|
|
35
|
+
throw new Error("NO_CREDITS");
|
|
36
|
+
}
|
|
37
|
+
|
|
30
38
|
const current = docSnap.data().credits as number;
|
|
31
|
-
if (current < cost)
|
|
32
|
-
|
|
39
|
+
if (current < cost) {
|
|
40
|
+
throw new Error("CREDITS_EXHAUSTED");
|
|
41
|
+
}
|
|
42
|
+
|
|
33
43
|
const updated = current - cost;
|
|
34
|
-
tx.update(ref, {
|
|
35
|
-
|
|
36
|
-
|
|
44
|
+
tx.update(ref, {
|
|
45
|
+
credits: updated,
|
|
46
|
+
lastUpdatedAt: serverTimestamp()
|
|
37
47
|
});
|
|
38
|
-
|
|
48
|
+
|
|
39
49
|
return updated;
|
|
40
50
|
});
|
|
41
51
|
|
|
42
52
|
// Emit event via EventBus (Observer Pattern)
|
|
43
53
|
subscriptionEventBus.emit(SUBSCRIPTION_EVENTS.CREDITS_UPDATED, userId);
|
|
44
54
|
|
|
45
|
-
return {
|
|
55
|
+
return {
|
|
56
|
+
success: true,
|
|
57
|
+
remainingCredits: remaining,
|
|
58
|
+
error: null
|
|
59
|
+
};
|
|
46
60
|
} catch (e: unknown) {
|
|
47
61
|
const message = e instanceof Error ? e.message : String(e);
|
|
48
62
|
const code = message === "NO_CREDITS" || message === "CREDITS_EXHAUSTED" ? message : "DEDUCT_ERR";
|
|
49
|
-
return {
|
|
63
|
+
return {
|
|
64
|
+
success: false,
|
|
65
|
+
remainingCredits: null,
|
|
66
|
+
error: { message, code }
|
|
67
|
+
};
|
|
50
68
|
}
|
|
51
69
|
}
|
|
52
70
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Timestamp } from "firebase/firestore";
|
|
2
|
-
import type {
|
|
3
|
-
PurchaseType,
|
|
2
|
+
import type {
|
|
3
|
+
PurchaseType,
|
|
4
4
|
PurchaseMetadata,
|
|
5
5
|
UserCreditsDocumentRead,
|
|
6
6
|
PurchaseSource
|
|
@@ -8,37 +8,31 @@ import type {
|
|
|
8
8
|
import { detectPackageType } from "../../../utils/packageTypeDetector";
|
|
9
9
|
|
|
10
10
|
export interface MetadataGeneratorConfig {
|
|
11
|
-
productId
|
|
12
|
-
source
|
|
13
|
-
type
|
|
11
|
+
productId: string;
|
|
12
|
+
source: PurchaseSource;
|
|
13
|
+
type: PurchaseType;
|
|
14
14
|
creditLimit: number;
|
|
15
15
|
platform: "ios" | "android";
|
|
16
|
-
appVersion
|
|
16
|
+
appVersion: string;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
export class PurchaseMetadataGenerator {
|
|
20
20
|
static generate(
|
|
21
21
|
config: MetadataGeneratorConfig,
|
|
22
|
-
existingData: UserCreditsDocumentRead
|
|
22
|
+
existingData: UserCreditsDocumentRead
|
|
23
23
|
): { purchaseType: PurchaseType; purchaseHistory: PurchaseMetadata[] } {
|
|
24
24
|
const { productId, source, type, creditLimit, platform, appVersion } = config;
|
|
25
|
-
|
|
26
|
-
if (!productId || !source) {
|
|
27
|
-
return {
|
|
28
|
-
purchaseType: type ?? "initial",
|
|
29
|
-
purchaseHistory: existingData?.purchaseHistory || []
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
25
|
|
|
33
26
|
const packageType = detectPackageType(productId);
|
|
34
|
-
let purchaseType: PurchaseType = type
|
|
27
|
+
let purchaseType: PurchaseType = type;
|
|
35
28
|
|
|
36
|
-
if (
|
|
37
|
-
const oldLimit = existingData.creditLimit
|
|
38
|
-
if (creditLimit > oldLimit)
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
29
|
+
if (packageType !== "unknown") {
|
|
30
|
+
const oldLimit = existingData.creditLimit;
|
|
31
|
+
if (creditLimit > oldLimit) {
|
|
32
|
+
purchaseType = "upgrade";
|
|
33
|
+
} else if (creditLimit < oldLimit) {
|
|
34
|
+
purchaseType = "downgrade";
|
|
35
|
+
}
|
|
42
36
|
}
|
|
43
37
|
|
|
44
38
|
const newMetadata: PurchaseMetadata = {
|
|
@@ -49,10 +43,10 @@ export class PurchaseMetadataGenerator {
|
|
|
49
43
|
type: purchaseType,
|
|
50
44
|
platform,
|
|
51
45
|
appVersion,
|
|
52
|
-
timestamp: Timestamp.fromDate(new Date())
|
|
46
|
+
timestamp: Timestamp.fromDate(new Date()),
|
|
53
47
|
};
|
|
54
48
|
|
|
55
|
-
const purchaseHistory = [...
|
|
49
|
+
const purchaseHistory = [...existingData.purchaseHistory, newMetadata].slice(-10);
|
|
56
50
|
|
|
57
51
|
return { purchaseType, purchaseHistory };
|
|
58
52
|
}
|
|
@@ -6,22 +6,22 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { SubscriptionPackageType } from "../../../utils/packageTypeDetector";
|
|
9
|
-
import type {
|
|
10
|
-
SubscriptionStatusType,
|
|
11
|
-
PeriodType,
|
|
12
|
-
PackageType,
|
|
13
|
-
Platform,
|
|
14
|
-
PurchaseSource,
|
|
15
|
-
PurchaseType
|
|
9
|
+
import type {
|
|
10
|
+
SubscriptionStatusType,
|
|
11
|
+
PeriodType,
|
|
12
|
+
PackageType,
|
|
13
|
+
Platform,
|
|
14
|
+
PurchaseSource,
|
|
15
|
+
PurchaseType
|
|
16
16
|
} from "../../subscription/core/SubscriptionConstants";
|
|
17
17
|
|
|
18
|
-
export type {
|
|
19
|
-
SubscriptionStatusType,
|
|
20
|
-
PeriodType,
|
|
21
|
-
PackageType,
|
|
22
|
-
Platform,
|
|
23
|
-
PurchaseSource,
|
|
24
|
-
PurchaseType
|
|
18
|
+
export type {
|
|
19
|
+
SubscriptionStatusType,
|
|
20
|
+
PeriodType,
|
|
21
|
+
PackageType,
|
|
22
|
+
Platform,
|
|
23
|
+
PurchaseSource,
|
|
24
|
+
PurchaseType
|
|
25
25
|
};
|
|
26
26
|
|
|
27
27
|
export type CreditType = "text" | "image";
|
|
@@ -36,30 +36,31 @@ export interface UserCredits {
|
|
|
36
36
|
purchasedAt: Date | null;
|
|
37
37
|
expirationDate: Date | null;
|
|
38
38
|
lastUpdatedAt: Date | null;
|
|
39
|
+
lastPurchaseAt: Date | null;
|
|
39
40
|
|
|
40
41
|
// RevenueCat subscription details
|
|
41
|
-
willRenew: boolean;
|
|
42
|
-
productId
|
|
43
|
-
packageType
|
|
44
|
-
originalTransactionId
|
|
42
|
+
willRenew: boolean | null;
|
|
43
|
+
productId: string | null;
|
|
44
|
+
packageType: PackageType | null;
|
|
45
|
+
originalTransactionId: string | null;
|
|
45
46
|
|
|
46
47
|
// Trial fields
|
|
47
|
-
periodType
|
|
48
|
-
isTrialing
|
|
49
|
-
trialStartDate
|
|
50
|
-
trialEndDate
|
|
51
|
-
trialCredits
|
|
52
|
-
convertedFromTrial
|
|
48
|
+
periodType: PeriodType | null;
|
|
49
|
+
isTrialing: boolean | null;
|
|
50
|
+
trialStartDate: Date | null;
|
|
51
|
+
trialEndDate: Date | null;
|
|
52
|
+
trialCredits: number | null;
|
|
53
|
+
convertedFromTrial: boolean | null;
|
|
53
54
|
|
|
54
55
|
// Credits
|
|
55
56
|
credits: number;
|
|
56
|
-
creditLimit
|
|
57
|
+
creditLimit: number;
|
|
57
58
|
|
|
58
59
|
// Metadata
|
|
59
|
-
purchaseSource
|
|
60
|
-
purchaseType
|
|
61
|
-
platform
|
|
62
|
-
appVersion
|
|
60
|
+
purchaseSource: PurchaseSource | null;
|
|
61
|
+
purchaseType: PurchaseType | null;
|
|
62
|
+
platform: Platform;
|
|
63
|
+
appVersion: string | null;
|
|
63
64
|
}
|
|
64
65
|
|
|
65
66
|
export interface CreditAllocation {
|
|
@@ -75,28 +76,27 @@ export interface CreditsConfig {
|
|
|
75
76
|
collectionName: string;
|
|
76
77
|
creditLimit: number;
|
|
77
78
|
/** When true, stores credits at users/{userId}/credits instead of {collectionName}/{userId} */
|
|
78
|
-
useUserSubcollection
|
|
79
|
+
useUserSubcollection: boolean;
|
|
79
80
|
/** Credit amounts per product ID for consumable credit packages */
|
|
80
|
-
creditPackageAmounts
|
|
81
|
+
creditPackageAmounts: Record<string, number>;
|
|
81
82
|
/** Credit allocations for different subscription types (weekly, monthly, yearly) */
|
|
82
|
-
packageAllocations
|
|
83
|
+
packageAllocations: PackageAllocationMap;
|
|
83
84
|
}
|
|
84
85
|
|
|
85
86
|
export interface CreditsResult<T = UserCredits> {
|
|
86
87
|
success: boolean;
|
|
87
|
-
data
|
|
88
|
-
error
|
|
88
|
+
data: T | null;
|
|
89
|
+
error: {
|
|
89
90
|
message: string;
|
|
90
91
|
code: string;
|
|
91
|
-
};
|
|
92
|
+
} | null;
|
|
92
93
|
}
|
|
93
94
|
|
|
94
95
|
export interface DeductCreditsResult {
|
|
95
96
|
success: boolean;
|
|
96
|
-
remainingCredits
|
|
97
|
-
error
|
|
97
|
+
remainingCredits: number | null;
|
|
98
|
+
error: {
|
|
98
99
|
message: string;
|
|
99
100
|
code: string;
|
|
100
|
-
};
|
|
101
|
+
} | null;
|
|
101
102
|
}
|
|
102
|
-
|
|
@@ -6,8 +6,8 @@ import type { UserCreditsDocumentRead } from "./UserCreditsDocument";
|
|
|
6
6
|
/** Maps Firestore document to domain entity with expiration validation */
|
|
7
7
|
export class CreditsMapper {
|
|
8
8
|
static toEntity(doc: UserCreditsDocumentRead): UserCredits {
|
|
9
|
-
const expirationDate = doc.expirationDate
|
|
10
|
-
const periodType = doc.periodType
|
|
9
|
+
const expirationDate = doc.expirationDate ? doc.expirationDate.toDate() : null;
|
|
10
|
+
const periodType = doc.periodType;
|
|
11
11
|
|
|
12
12
|
// Validate isPremium against expirationDate (real-time check)
|
|
13
13
|
const { isPremium, status } = CreditsMapper.validateSubscription(doc, expirationDate, periodType);
|
|
@@ -18,12 +18,13 @@ export class CreditsMapper {
|
|
|
18
18
|
status,
|
|
19
19
|
|
|
20
20
|
// Dates
|
|
21
|
-
purchasedAt: doc.purchasedAt
|
|
21
|
+
purchasedAt: doc.purchasedAt.toDate(),
|
|
22
22
|
expirationDate,
|
|
23
|
-
lastUpdatedAt: doc.lastUpdatedAt
|
|
23
|
+
lastUpdatedAt: doc.lastUpdatedAt.toDate(),
|
|
24
|
+
lastPurchaseAt: doc.lastPurchaseAt ? doc.lastPurchaseAt.toDate() : null,
|
|
24
25
|
|
|
25
26
|
// RevenueCat details
|
|
26
|
-
willRenew: doc.willRenew
|
|
27
|
+
willRenew: doc.willRenew,
|
|
27
28
|
productId: doc.productId,
|
|
28
29
|
packageType: doc.packageType,
|
|
29
30
|
originalTransactionId: doc.originalTransactionId,
|
|
@@ -31,8 +32,8 @@ export class CreditsMapper {
|
|
|
31
32
|
// Trial fields
|
|
32
33
|
periodType,
|
|
33
34
|
isTrialing: doc.isTrialing,
|
|
34
|
-
trialStartDate: doc.trialStartDate
|
|
35
|
-
trialEndDate: doc.trialEndDate
|
|
35
|
+
trialStartDate: doc.trialStartDate ? doc.trialStartDate.toDate() : null,
|
|
36
|
+
trialEndDate: doc.trialEndDate ? doc.trialEndDate.toDate() : null,
|
|
36
37
|
trialCredits: doc.trialCredits,
|
|
37
38
|
convertedFromTrial: doc.convertedFromTrial,
|
|
38
39
|
|
|
@@ -52,9 +53,9 @@ export class CreditsMapper {
|
|
|
52
53
|
private static validateSubscription(
|
|
53
54
|
doc: UserCreditsDocumentRead,
|
|
54
55
|
expirationDate: Date | null,
|
|
55
|
-
periodType
|
|
56
|
+
periodType: PeriodType | null
|
|
56
57
|
): { isPremium: boolean; status: SubscriptionStatusType } {
|
|
57
|
-
const isPremium = doc.isPremium
|
|
58
|
+
const isPremium = doc.isPremium;
|
|
58
59
|
const willRenew = doc.willRenew ?? false;
|
|
59
60
|
const isExpired = expirationDate ? expirationDate < new Date() : false;
|
|
60
61
|
|
|
@@ -62,7 +63,7 @@ export class CreditsMapper {
|
|
|
62
63
|
isPremium,
|
|
63
64
|
willRenew,
|
|
64
65
|
isExpired,
|
|
65
|
-
periodType,
|
|
66
|
+
periodType: periodType ?? undefined,
|
|
66
67
|
});
|
|
67
68
|
|
|
68
69
|
// Override isPremium if expired
|
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
PurchaseSource,
|
|
3
|
-
PurchaseType,
|
|
4
|
-
SubscriptionStatusType,
|
|
1
|
+
import type {
|
|
2
|
+
PurchaseSource,
|
|
3
|
+
PurchaseType,
|
|
4
|
+
SubscriptionStatusType,
|
|
5
5
|
PeriodType,
|
|
6
6
|
PackageType,
|
|
7
7
|
Platform
|
|
8
8
|
} from "../../subscription/core/SubscriptionConstants";
|
|
9
9
|
|
|
10
|
-
export type {
|
|
11
|
-
PurchaseSource,
|
|
12
|
-
PurchaseType,
|
|
13
|
-
SubscriptionStatusType,
|
|
14
|
-
PeriodType
|
|
10
|
+
export type {
|
|
11
|
+
PurchaseSource,
|
|
12
|
+
PurchaseType,
|
|
13
|
+
SubscriptionStatusType,
|
|
14
|
+
PeriodType
|
|
15
15
|
};
|
|
16
16
|
|
|
17
17
|
export interface FirestoreTimestamp {
|
|
@@ -25,45 +25,45 @@ export interface PurchaseMetadata {
|
|
|
25
25
|
source: PurchaseSource;
|
|
26
26
|
type: PurchaseType;
|
|
27
27
|
platform: Platform;
|
|
28
|
-
appVersion
|
|
28
|
+
appVersion: string;
|
|
29
29
|
timestamp: FirestoreTimestamp;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
/** Single Source of Truth for user subscription data */
|
|
33
33
|
export interface UserCreditsDocumentRead {
|
|
34
34
|
// Core subscription status
|
|
35
|
-
isPremium
|
|
36
|
-
status
|
|
35
|
+
isPremium: boolean;
|
|
36
|
+
status: SubscriptionStatusType;
|
|
37
37
|
|
|
38
38
|
// Dates (all from RevenueCat)
|
|
39
|
-
purchasedAt
|
|
40
|
-
expirationDate
|
|
41
|
-
lastUpdatedAt
|
|
42
|
-
lastPurchaseAt
|
|
39
|
+
purchasedAt: FirestoreTimestamp;
|
|
40
|
+
expirationDate: FirestoreTimestamp | null;
|
|
41
|
+
lastUpdatedAt: FirestoreTimestamp;
|
|
42
|
+
lastPurchaseAt: FirestoreTimestamp | null;
|
|
43
43
|
|
|
44
44
|
// RevenueCat subscription details
|
|
45
|
-
willRenew
|
|
46
|
-
productId
|
|
47
|
-
packageType
|
|
48
|
-
originalTransactionId
|
|
45
|
+
willRenew: boolean | null;
|
|
46
|
+
productId: string | null;
|
|
47
|
+
packageType: PackageType | null;
|
|
48
|
+
originalTransactionId: string | null;
|
|
49
49
|
|
|
50
50
|
// Trial fields
|
|
51
|
-
periodType
|
|
52
|
-
isTrialing
|
|
53
|
-
trialStartDate
|
|
54
|
-
trialEndDate
|
|
55
|
-
trialCredits
|
|
56
|
-
convertedFromTrial
|
|
51
|
+
periodType: PeriodType | null;
|
|
52
|
+
isTrialing: boolean | null;
|
|
53
|
+
trialStartDate: FirestoreTimestamp | null;
|
|
54
|
+
trialEndDate: FirestoreTimestamp | null;
|
|
55
|
+
trialCredits: number | null;
|
|
56
|
+
convertedFromTrial: boolean | null;
|
|
57
57
|
|
|
58
58
|
// Credits
|
|
59
59
|
credits: number;
|
|
60
|
-
creditLimit
|
|
60
|
+
creditLimit: number;
|
|
61
61
|
|
|
62
62
|
// Metadata
|
|
63
|
-
purchaseSource
|
|
64
|
-
purchaseType
|
|
65
|
-
platform
|
|
66
|
-
appVersion
|
|
67
|
-
processedPurchases
|
|
68
|
-
purchaseHistory
|
|
63
|
+
purchaseSource: PurchaseSource | null;
|
|
64
|
+
purchaseType: PurchaseType | null;
|
|
65
|
+
platform: Platform;
|
|
66
|
+
appVersion: string | null;
|
|
67
|
+
processedPurchases: string[];
|
|
68
|
+
purchaseHistory: PurchaseMetadata[];
|
|
69
69
|
}
|