@umituz/react-native-subscription 2.33.0 → 2.33.1
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/infrastructure/operations/CreditsFetcher.ts +1 -2
- package/src/domains/credits/infrastructure/operations/CreditsInitializer.ts +1 -1
- package/src/domains/credits/presentation/deduct-credit/index.ts +2 -0
- package/src/domains/credits/presentation/deduct-credit/mutationConfig.ts +81 -0
- package/src/domains/credits/presentation/deduct-credit/types.ts +11 -0
- package/src/domains/credits/presentation/deduct-credit/useDeductCredit.ts +44 -0
- package/src/domains/subscription/application/initializer/BackgroundInitializer.ts +21 -0
- package/src/domains/subscription/application/initializer/ConfigValidator.ts +33 -0
- package/src/domains/subscription/application/initializer/ServiceConfigurator.ts +45 -0
- package/src/domains/subscription/application/initializer/SubscriptionInitializer.ts +11 -0
- package/src/domains/subscription/application/initializer/index.ts +2 -0
- package/src/domains/subscription/infrastructure/handlers/PackageHandler.ts +13 -94
- package/src/domains/subscription/infrastructure/handlers/package-operations/PackageFetcher.ts +57 -0
- package/src/domains/subscription/infrastructure/handlers/package-operations/PackagePurchaser.ts +15 -0
- package/src/domains/subscription/infrastructure/handlers/package-operations/PackageRestorer.ts +34 -0
- package/src/domains/subscription/infrastructure/handlers/package-operations/PremiumStatusChecker.ts +9 -0
- package/src/domains/subscription/infrastructure/handlers/package-operations/index.ts +5 -0
- package/src/domains/subscription/infrastructure/handlers/package-operations/types.ts +4 -0
- package/src/domains/subscription/infrastructure/hooks/customer-info/index.ts +2 -0
- package/src/domains/subscription/infrastructure/hooks/customer-info/types.ts +9 -0
- package/src/domains/subscription/infrastructure/hooks/customer-info/useCustomerInfo.ts +57 -0
- package/src/domains/subscription/infrastructure/services/listeners/CustomerInfoHandler.ts +1 -1
- package/src/domains/subscription/infrastructure/services/listeners/ListenerState.ts +1 -1
- package/src/domains/subscription/infrastructure/services/purchase/PurchaseErrorHandler.ts +0 -1
- package/src/domains/subscription/infrastructure/utils/renewal/PackageTierComparator.ts +14 -0
- package/src/domains/subscription/infrastructure/utils/renewal/RenewalDetector.ts +78 -0
- package/src/domains/subscription/infrastructure/utils/renewal/RenewalStateUpdater.ts +11 -0
- package/src/domains/subscription/infrastructure/utils/renewal/index.ts +3 -0
- package/src/domains/subscription/infrastructure/utils/renewal/types.ts +14 -0
- package/src/domains/wallet/index.ts +2 -2
- package/src/domains/wallet/infrastructure/repositories/transaction/CollectionBuilder.ts +14 -0
- package/src/domains/wallet/infrastructure/repositories/transaction/TransactionFetcher.ts +46 -0
- package/src/domains/wallet/infrastructure/repositories/transaction/TransactionRepository.ts +34 -0
- package/src/domains/wallet/infrastructure/repositories/transaction/TransactionWriter.ts +43 -0
- package/src/domains/wallet/infrastructure/repositories/transaction/index.ts +10 -0
- package/src/domains/wallet/infrastructure/services/product-metadata/CacheManager.ts +30 -0
- package/src/domains/wallet/infrastructure/services/product-metadata/FirebaseFetcher.ts +17 -0
- package/src/domains/wallet/infrastructure/services/product-metadata/ProductMetadataService.ts +57 -0
- package/src/domains/wallet/infrastructure/services/product-metadata/ServiceManager.ts +29 -0
- package/src/domains/wallet/infrastructure/services/product-metadata/index.ts +7 -0
- package/src/domains/wallet/presentation/hooks/useProductMetadata.ts +1 -1
- package/src/domains/wallet/presentation/hooks/useTransactionHistory.ts +1 -1
- package/src/index.ts +2 -2
- package/src/init/createSubscriptionInitModule.ts +1 -1
- package/src/domains/credits/presentation/useDeductCredit.ts +0 -110
- package/src/domains/subscription/application/SubscriptionInitializer.ts +0 -112
- package/src/domains/subscription/infrastructure/hooks/useCustomerInfo.ts +0 -113
- package/src/domains/subscription/infrastructure/utils/RenewalDetector.ts +0 -141
- package/src/domains/wallet/infrastructure/repositories/TransactionRepository.ts +0 -114
- package/src/domains/wallet/infrastructure/services/ProductMetadataService.ts +0 -114
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Subscription Initializer
|
|
3
|
-
*
|
|
4
|
-
* Uses RevenueCat best practices:
|
|
5
|
-
* - Non-blocking initialization (fire and forget)
|
|
6
|
-
* - Relies on CustomerInfoUpdateListener for state updates
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { Platform } from "react-native";
|
|
10
|
-
import { configureCreditsRepository } from "../../credits/infrastructure/CreditsRepositoryManager";
|
|
11
|
-
import { SubscriptionManager } from "../infrastructure/managers/SubscriptionManager";
|
|
12
|
-
import { configureAuthProvider } from "../presentation/useAuthAwarePurchase";
|
|
13
|
-
import { SubscriptionSyncService } from "./SubscriptionSyncService";
|
|
14
|
-
import { getCurrentUserId, setupAuthStateListener } from "./SubscriptionAuthListener";
|
|
15
|
-
import type { SubscriptionInitConfig } from "./SubscriptionInitializerTypes";
|
|
16
|
-
|
|
17
|
-
export type { FirebaseAuthLike, CreditPackageConfig, SubscriptionInitConfig } from "./SubscriptionInitializerTypes";
|
|
18
|
-
|
|
19
|
-
export const initializeSubscription = async (config: SubscriptionInitConfig): Promise<void> => {
|
|
20
|
-
const {
|
|
21
|
-
apiKey,
|
|
22
|
-
apiKeyIos,
|
|
23
|
-
apiKeyAndroid,
|
|
24
|
-
entitlementId,
|
|
25
|
-
credits,
|
|
26
|
-
getAnonymousUserId,
|
|
27
|
-
getFirebaseAuth,
|
|
28
|
-
showAuthModal,
|
|
29
|
-
onCreditsUpdated,
|
|
30
|
-
creditPackages,
|
|
31
|
-
} = config;
|
|
32
|
-
|
|
33
|
-
const key = Platform.OS === 'ios'
|
|
34
|
-
? (apiKeyIos || apiKey)
|
|
35
|
-
: (apiKeyAndroid || apiKey);
|
|
36
|
-
|
|
37
|
-
if (!key) {
|
|
38
|
-
throw new Error('API key required');
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
if (!creditPackages) {
|
|
42
|
-
throw new Error('creditPackages is required');
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
if (!creditPackages.identifierPattern) {
|
|
46
|
-
throw new Error('creditPackages.identifierPattern is required');
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
if (!creditPackages.amounts) {
|
|
50
|
-
throw new Error('creditPackages.amounts is required');
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
if (!getAnonymousUserId) {
|
|
54
|
-
throw new Error('getAnonymousUserId is required');
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// 1. Configure Repository
|
|
58
|
-
configureCreditsRepository({
|
|
59
|
-
...credits,
|
|
60
|
-
creditPackageAmounts: creditPackages.amounts
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
// 2. Setup Sync Service
|
|
64
|
-
const syncService = new SubscriptionSyncService(entitlementId);
|
|
65
|
-
|
|
66
|
-
// 3. Configure Subscription Manager
|
|
67
|
-
SubscriptionManager.configure({
|
|
68
|
-
config: {
|
|
69
|
-
apiKey: key,
|
|
70
|
-
entitlementIdentifier: entitlementId,
|
|
71
|
-
consumableProductIdentifiers: [creditPackages.identifierPattern],
|
|
72
|
-
onPurchaseCompleted: (u: string, p: string, c: any, s: any) => syncService.handlePurchase(u, p, c, s),
|
|
73
|
-
onRenewalDetected: (u: string, p: string, expires: string, c: any) => syncService.handleRenewal(u, p, expires, c),
|
|
74
|
-
onPremiumStatusChanged: (u: string, isP: boolean, pId: any, exp: any, willR: any, pt: any) => syncService.handlePremiumStatusChanged(u, isP, pId, exp, willR, pt),
|
|
75
|
-
onCreditsUpdated,
|
|
76
|
-
},
|
|
77
|
-
apiKey: key,
|
|
78
|
-
getAnonymousUserId,
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
// 4. Configure Auth aware actions
|
|
82
|
-
configureAuthProvider({
|
|
83
|
-
isAuthenticated: () => {
|
|
84
|
-
const auth = getFirebaseAuth();
|
|
85
|
-
if (!auth) {
|
|
86
|
-
throw new Error("Firebase auth is not available");
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const u = auth.currentUser;
|
|
90
|
-
return !!(u && !u.isAnonymous);
|
|
91
|
-
},
|
|
92
|
-
showAuthModal,
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
const initializeInBackground = async (userId?: string): Promise<void> => {
|
|
96
|
-
await SubscriptionManager.initialize(userId);
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
// 5. Start Background Init
|
|
100
|
-
const auth = getFirebaseAuth();
|
|
101
|
-
if (!auth) {
|
|
102
|
-
throw new Error("Firebase auth is not available");
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
const initialUserId = getCurrentUserId(() => auth);
|
|
106
|
-
await initializeInBackground(initialUserId);
|
|
107
|
-
|
|
108
|
-
// 6. Listen for Auth Changes
|
|
109
|
-
setupAuthStateListener(() => auth, (newUserId) => {
|
|
110
|
-
initializeInBackground(newUserId);
|
|
111
|
-
});
|
|
112
|
-
};
|
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* useCustomerInfo Hook
|
|
3
|
-
* Fetches and manages RevenueCat CustomerInfo with real-time updates
|
|
4
|
-
*
|
|
5
|
-
* BEST PRACTICE: Always get expiration date from CustomerInfo (source of truth)
|
|
6
|
-
* Never calculate expiration dates client-side (purchaseDate + 1 year is WRONG)
|
|
7
|
-
*
|
|
8
|
-
* This hook provides:
|
|
9
|
-
* - Initial fetch from SDK cache (instant, no network)
|
|
10
|
-
* - Real-time listener for updates (renewals, purchases, restore)
|
|
11
|
-
* - Automatic cleanup on unmount
|
|
12
|
-
* - SDK caches CustomerInfo and fetches every ~5 minutes
|
|
13
|
-
*
|
|
14
|
-
* @see https://www.revenuecat.com/docs/customers/customer-info
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
import { useEffect, useState, useCallback, useRef } from "react";
|
|
18
|
-
import Purchases, { type CustomerInfo } from "react-native-purchases";
|
|
19
|
-
|
|
20
|
-
export interface UseCustomerInfoResult {
|
|
21
|
-
/** Current CustomerInfo from RevenueCat SDK */
|
|
22
|
-
customerInfo: CustomerInfo | null;
|
|
23
|
-
/** Loading state (only true on initial fetch) */
|
|
24
|
-
loading: boolean;
|
|
25
|
-
/** Error message if fetch failed */
|
|
26
|
-
error: string | null;
|
|
27
|
-
/** Manually refetch CustomerInfo (usually not needed, listener handles updates) */
|
|
28
|
-
refetch: () => Promise<void>;
|
|
29
|
-
/** Whether SDK is currently fetching */
|
|
30
|
-
isFetching: boolean;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Hook to get CustomerInfo from RevenueCat SDK
|
|
35
|
-
*
|
|
36
|
-
* Features:
|
|
37
|
-
* - SDK cache: First call returns cached data (instant)
|
|
38
|
-
* - Auto-updates: Listener triggers on renewals, purchases, restore
|
|
39
|
-
* - Network fetch: SDK fetches every ~5 minutes in background
|
|
40
|
-
* - Grace periods: Expiration dates include grace period automatically
|
|
41
|
-
*
|
|
42
|
-
* @example
|
|
43
|
-
* ```typescript
|
|
44
|
-
* const { customerInfo, loading } = useCustomerInfo();
|
|
45
|
-
*
|
|
46
|
-
* // Check premium status
|
|
47
|
-
* const isPremium = !!customerInfo?.entitlements.active['premium'];
|
|
48
|
-
*
|
|
49
|
-
* // Get expiration date (ALWAYS from CustomerInfo, NEVER calculate!)
|
|
50
|
-
* const expiresAt = customerInfo?.entitlements.active['premium']?.expirationDate;
|
|
51
|
-
*
|
|
52
|
-
* // Check will renew
|
|
53
|
-
* const willRenew = customerInfo?.entitlements.active['premium']?.willRenew;
|
|
54
|
-
* ```
|
|
55
|
-
*
|
|
56
|
-
* @returns CustomerInfo and loading state
|
|
57
|
-
*/
|
|
58
|
-
export function useCustomerInfo(): UseCustomerInfoResult {
|
|
59
|
-
const [customerInfo, setCustomerInfo] = useState<CustomerInfo | null>(null);
|
|
60
|
-
const [loading, setLoading] = useState(true);
|
|
61
|
-
const [isFetching, setIsFetching] = useState(false);
|
|
62
|
-
const [error, setError] = useState<string | null>(null);
|
|
63
|
-
|
|
64
|
-
const fetchCustomerInfo = useCallback(async () => {
|
|
65
|
-
try {
|
|
66
|
-
setIsFetching(true);
|
|
67
|
-
setError(null);
|
|
68
|
-
|
|
69
|
-
// SDK returns cached data instantly if available
|
|
70
|
-
// Network fetch happens in background automatically
|
|
71
|
-
const info = await Purchases.getCustomerInfo();
|
|
72
|
-
|
|
73
|
-
setCustomerInfo(info);
|
|
74
|
-
} catch (err) {
|
|
75
|
-
const errorMessage =
|
|
76
|
-
err instanceof Error ? err.message : "Failed to fetch customer info";
|
|
77
|
-
setError(errorMessage);
|
|
78
|
-
} finally {
|
|
79
|
-
setLoading(false);
|
|
80
|
-
setIsFetching(false);
|
|
81
|
-
}
|
|
82
|
-
}, []);
|
|
83
|
-
|
|
84
|
-
const listenerRef = useRef<((info: CustomerInfo) => void) | null>(null);
|
|
85
|
-
|
|
86
|
-
useEffect(() => {
|
|
87
|
-
fetchCustomerInfo();
|
|
88
|
-
|
|
89
|
-
const listener = (info: CustomerInfo) => {
|
|
90
|
-
setCustomerInfo(info);
|
|
91
|
-
setError(null);
|
|
92
|
-
};
|
|
93
|
-
|
|
94
|
-
// Set ref BEFORE adding listener to ensure cleanup can always find it
|
|
95
|
-
listenerRef.current = listener;
|
|
96
|
-
Purchases.addCustomerInfoUpdateListener(listener);
|
|
97
|
-
|
|
98
|
-
return () => {
|
|
99
|
-
if (listenerRef.current) {
|
|
100
|
-
Purchases.removeCustomerInfoUpdateListener(listenerRef.current);
|
|
101
|
-
listenerRef.current = null;
|
|
102
|
-
}
|
|
103
|
-
};
|
|
104
|
-
}, [fetchCustomerInfo]);
|
|
105
|
-
|
|
106
|
-
return {
|
|
107
|
-
customerInfo,
|
|
108
|
-
loading,
|
|
109
|
-
error,
|
|
110
|
-
refetch: fetchCustomerInfo,
|
|
111
|
-
isFetching,
|
|
112
|
-
};
|
|
113
|
-
}
|
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Renewal Detector
|
|
3
|
-
* Detects subscription renewals by tracking expiration date changes
|
|
4
|
-
* Best Practice: Compare expiration dates to detect renewal events
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import type { CustomerInfo } from "react-native-purchases";
|
|
8
|
-
import { detectPackageType } from "../../../../utils/packageTypeDetector";
|
|
9
|
-
|
|
10
|
-
export interface RenewalState {
|
|
11
|
-
previousExpirationDate: string | null;
|
|
12
|
-
previousProductId: string | null;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export interface RenewalDetectionResult {
|
|
16
|
-
isRenewal: boolean;
|
|
17
|
-
isPlanChange: boolean;
|
|
18
|
-
isUpgrade: boolean;
|
|
19
|
-
isDowngrade: boolean;
|
|
20
|
-
productId: string | null;
|
|
21
|
-
previousProductId: string | null;
|
|
22
|
-
newExpirationDate: string | null;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const PACKAGE_TIER_ORDER: Record<string, number> = {
|
|
26
|
-
weekly: 1,
|
|
27
|
-
monthly: 2,
|
|
28
|
-
yearly: 3,
|
|
29
|
-
unknown: 0,
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
function getPackageTier(productId: string | null): number {
|
|
33
|
-
if (!productId) return 0;
|
|
34
|
-
const packageType = detectPackageType(productId);
|
|
35
|
-
return PACKAGE_TIER_ORDER[packageType] ?? 0;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Detects if a subscription renewal or plan change occurred
|
|
40
|
-
*
|
|
41
|
-
* Best Practice (RevenueCat):
|
|
42
|
-
* - Track previous expiration date
|
|
43
|
-
* - If new expiration > previous → Renewal detected
|
|
44
|
-
* - If productId changed → Plan change (upgrade/downgrade)
|
|
45
|
-
* - Reset credits on renewal or plan change (industry standard)
|
|
46
|
-
*
|
|
47
|
-
* @param state Previous state (expiration date, product ID)
|
|
48
|
-
* @param customerInfo Current CustomerInfo from RevenueCat
|
|
49
|
-
* @param entitlementId Entitlement identifier to check
|
|
50
|
-
* @returns Renewal detection result
|
|
51
|
-
*/
|
|
52
|
-
export function detectRenewal(
|
|
53
|
-
state: RenewalState,
|
|
54
|
-
customerInfo: CustomerInfo,
|
|
55
|
-
entitlementId: string
|
|
56
|
-
): RenewalDetectionResult {
|
|
57
|
-
const entitlement = customerInfo.entitlements.active[entitlementId];
|
|
58
|
-
|
|
59
|
-
const baseResult: RenewalDetectionResult = {
|
|
60
|
-
isRenewal: false,
|
|
61
|
-
isPlanChange: false,
|
|
62
|
-
isUpgrade: false,
|
|
63
|
-
isDowngrade: false,
|
|
64
|
-
productId: null,
|
|
65
|
-
previousProductId: state.previousProductId,
|
|
66
|
-
newExpirationDate: null,
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
if (!entitlement) {
|
|
70
|
-
return baseResult;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const newExpirationDate = entitlement.expirationDate;
|
|
74
|
-
const productId = entitlement.productIdentifier;
|
|
75
|
-
|
|
76
|
-
// First time seeing this subscription - not a renewal
|
|
77
|
-
if (!state.previousExpirationDate || !state.previousProductId) {
|
|
78
|
-
return {
|
|
79
|
-
...baseResult,
|
|
80
|
-
productId,
|
|
81
|
-
newExpirationDate,
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
if (!newExpirationDate) {
|
|
86
|
-
// Lifetime subscription (no expiration) - not a renewal
|
|
87
|
-
return {
|
|
88
|
-
...baseResult,
|
|
89
|
-
productId,
|
|
90
|
-
newExpirationDate,
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
const newExpiration = new Date(newExpirationDate);
|
|
94
|
-
const previousExpiration = new Date(state.previousExpirationDate);
|
|
95
|
-
const productChanged = productId !== state.previousProductId;
|
|
96
|
-
const expirationExtended = newExpiration > previousExpiration;
|
|
97
|
-
|
|
98
|
-
// Plan change detection (upgrade/downgrade)
|
|
99
|
-
if (productChanged) {
|
|
100
|
-
const oldTier = getPackageTier(state.previousProductId);
|
|
101
|
-
const newTier = getPackageTier(productId);
|
|
102
|
-
const isUpgrade = newTier > oldTier;
|
|
103
|
-
const isDowngrade = newTier < oldTier;
|
|
104
|
-
|
|
105
|
-
return {
|
|
106
|
-
isRenewal: false,
|
|
107
|
-
isPlanChange: true,
|
|
108
|
-
isUpgrade,
|
|
109
|
-
isDowngrade,
|
|
110
|
-
productId,
|
|
111
|
-
previousProductId: state.previousProductId,
|
|
112
|
-
newExpirationDate,
|
|
113
|
-
};
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// Same product renewal
|
|
117
|
-
const isRenewal = expirationExtended;
|
|
118
|
-
|
|
119
|
-
return {
|
|
120
|
-
isRenewal,
|
|
121
|
-
isPlanChange: false,
|
|
122
|
-
isUpgrade: false,
|
|
123
|
-
isDowngrade: false,
|
|
124
|
-
productId,
|
|
125
|
-
previousProductId: state.previousProductId,
|
|
126
|
-
newExpirationDate,
|
|
127
|
-
};
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* Updates renewal state after detection
|
|
132
|
-
*/
|
|
133
|
-
export function updateRenewalState(
|
|
134
|
-
_state: RenewalState,
|
|
135
|
-
result: RenewalDetectionResult
|
|
136
|
-
): RenewalState {
|
|
137
|
-
return {
|
|
138
|
-
previousExpirationDate: result.newExpirationDate,
|
|
139
|
-
previousProductId: result.productId,
|
|
140
|
-
};
|
|
141
|
-
}
|
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Transaction Repository
|
|
3
|
-
*
|
|
4
|
-
* Firestore operations for credit transaction logs.
|
|
5
|
-
* Generic repository for use across hundreds of apps.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import {
|
|
9
|
-
getDocs,
|
|
10
|
-
addDoc,
|
|
11
|
-
query,
|
|
12
|
-
where,
|
|
13
|
-
orderBy,
|
|
14
|
-
limit as firestoreLimit,
|
|
15
|
-
serverTimestamp,
|
|
16
|
-
type QueryConstraint,
|
|
17
|
-
} from "firebase/firestore";
|
|
18
|
-
import { BaseRepository } from "@umituz/react-native-firebase";
|
|
19
|
-
import type {
|
|
20
|
-
CreditLog,
|
|
21
|
-
TransactionRepositoryConfig,
|
|
22
|
-
TransactionQueryOptions,
|
|
23
|
-
TransactionResult,
|
|
24
|
-
TransactionReason,
|
|
25
|
-
} from "../../domain/types/transaction.types";
|
|
26
|
-
import { TransactionMapper } from "../../domain/mappers/TransactionMapper";
|
|
27
|
-
import { requireFirestore, buildCollectionRef, type CollectionConfig, mapErrorToResult } from "../../../../shared/infrastructure/firestore";
|
|
28
|
-
|
|
29
|
-
export class TransactionRepository extends BaseRepository {
|
|
30
|
-
private config: TransactionRepositoryConfig;
|
|
31
|
-
|
|
32
|
-
constructor(config: TransactionRepositoryConfig) {
|
|
33
|
-
super(config.collectionName);
|
|
34
|
-
this.config = config;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
private getCollectionConfig(): CollectionConfig {
|
|
38
|
-
return {
|
|
39
|
-
collectionName: this.config.collectionName,
|
|
40
|
-
useUserSubcollection: this.config.useUserSubcollection ?? false,
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
private getCollectionRef(db: any, userId: string) {
|
|
45
|
-
const config = this.getCollectionConfig();
|
|
46
|
-
return buildCollectionRef(db, userId, config);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
async getTransactions(
|
|
50
|
-
options: TransactionQueryOptions
|
|
51
|
-
): Promise<TransactionResult> {
|
|
52
|
-
try {
|
|
53
|
-
const db = requireFirestore();
|
|
54
|
-
const colRef = this.getCollectionRef(db, options.userId);
|
|
55
|
-
const constraints: QueryConstraint[] = [];
|
|
56
|
-
|
|
57
|
-
if (!this.config.useUserSubcollection) {
|
|
58
|
-
constraints.push(where("userId", "==", options.userId));
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
constraints.push(orderBy("createdAt", "desc"));
|
|
62
|
-
constraints.push(firestoreLimit(options.limit ?? 50));
|
|
63
|
-
|
|
64
|
-
const q = query(colRef, ...constraints);
|
|
65
|
-
const snapshot = await getDocs(q);
|
|
66
|
-
|
|
67
|
-
const transactions: CreditLog[] = snapshot.docs.map((docSnap) =>
|
|
68
|
-
TransactionMapper.toEntity(docSnap, options.userId)
|
|
69
|
-
);
|
|
70
|
-
|
|
71
|
-
return { success: true, data: transactions };
|
|
72
|
-
} catch (error) {
|
|
73
|
-
return mapErrorToResult(error);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
async addTransaction(
|
|
78
|
-
userId: string,
|
|
79
|
-
change: number,
|
|
80
|
-
reason: TransactionReason,
|
|
81
|
-
metadata?: Partial<CreditLog>
|
|
82
|
-
): Promise<TransactionResult<CreditLog>> {
|
|
83
|
-
try {
|
|
84
|
-
const db = requireFirestore();
|
|
85
|
-
const colRef = this.getCollectionRef(db, userId);
|
|
86
|
-
const docData = {
|
|
87
|
-
...TransactionMapper.toFirestore(userId, change, reason, metadata),
|
|
88
|
-
createdAt: serverTimestamp(),
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
const docRef = await addDoc(colRef, docData);
|
|
92
|
-
|
|
93
|
-
return {
|
|
94
|
-
success: true,
|
|
95
|
-
data: {
|
|
96
|
-
id: docRef.id,
|
|
97
|
-
userId,
|
|
98
|
-
change,
|
|
99
|
-
reason,
|
|
100
|
-
...metadata,
|
|
101
|
-
createdAt: Date.now(),
|
|
102
|
-
},
|
|
103
|
-
};
|
|
104
|
-
} catch (error) {
|
|
105
|
-
return mapErrorToResult<CreditLog>(error);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
export function createTransactionRepository(
|
|
111
|
-
config: TransactionRepositoryConfig
|
|
112
|
-
): TransactionRepository {
|
|
113
|
-
return new TransactionRepository(config);
|
|
114
|
-
}
|
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Product Metadata Service
|
|
3
|
-
*
|
|
4
|
-
* Generic service for fetching product metadata from Firestore.
|
|
5
|
-
* Collection name is configurable for use across hundreds of apps.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { collection, getDocs, orderBy, query } from "firebase/firestore";
|
|
9
|
-
import { requireFirestore } from "../../../../shared/infrastructure";
|
|
10
|
-
import type {
|
|
11
|
-
ProductMetadata,
|
|
12
|
-
ProductMetadataConfig,
|
|
13
|
-
ProductType,
|
|
14
|
-
} from "../../domain/types/wallet.types";
|
|
15
|
-
|
|
16
|
-
interface CacheEntry {
|
|
17
|
-
data: ProductMetadata[];
|
|
18
|
-
timestamp: number;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const DEFAULT_CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
|
|
22
|
-
|
|
23
|
-
export class ProductMetadataService {
|
|
24
|
-
private config: ProductMetadataConfig;
|
|
25
|
-
private cache: CacheEntry | null = null;
|
|
26
|
-
|
|
27
|
-
constructor(config: ProductMetadataConfig) {
|
|
28
|
-
this.config = config;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
private isCacheValid(): boolean {
|
|
32
|
-
if (!this.cache) return false;
|
|
33
|
-
const ttl = this.config.cacheTTL ?? DEFAULT_CACHE_TTL_MS;
|
|
34
|
-
return Date.now() - this.cache.timestamp < ttl;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
private async fetchFromFirebase(): Promise<ProductMetadata[]> {
|
|
38
|
-
const db = requireFirestore();
|
|
39
|
-
const colRef = collection(db, this.config.collectionName);
|
|
40
|
-
const q = query(colRef, orderBy("order", "asc"));
|
|
41
|
-
const snapshot = await getDocs(q);
|
|
42
|
-
|
|
43
|
-
return snapshot.docs.map((docSnap) => ({
|
|
44
|
-
productId: docSnap.id,
|
|
45
|
-
...docSnap.data(),
|
|
46
|
-
})) as ProductMetadata[];
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
async getAll(): Promise<ProductMetadata[]> {
|
|
50
|
-
if (this.isCacheValid() && this.cache) {
|
|
51
|
-
return this.cache.data;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
try {
|
|
55
|
-
const data = await this.fetchFromFirebase();
|
|
56
|
-
this.cache = { data, timestamp: Date.now() };
|
|
57
|
-
return data;
|
|
58
|
-
} catch (error) {
|
|
59
|
-
if (this.cache) {
|
|
60
|
-
return this.cache.data;
|
|
61
|
-
}
|
|
62
|
-
throw error;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
async getByProductId(productId: string): Promise<ProductMetadata | null> {
|
|
67
|
-
const all = await this.getAll();
|
|
68
|
-
return all.find((p) => p.productId === productId) ?? null;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
async getByType(type: ProductType): Promise<ProductMetadata[]> {
|
|
72
|
-
const all = await this.getAll();
|
|
73
|
-
return all.filter((p) => p.type === type);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
async getCreditsPackages(): Promise<ProductMetadata[]> {
|
|
77
|
-
return this.getByType("credits");
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
async getSubscriptionPackages(): Promise<ProductMetadata[]> {
|
|
81
|
-
return this.getByType("subscription");
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
clearCache(): void {
|
|
85
|
-
this.cache = null;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
export function createProductMetadataService(
|
|
90
|
-
config: ProductMetadataConfig
|
|
91
|
-
): ProductMetadataService {
|
|
92
|
-
return new ProductMetadataService(config);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
let defaultService: ProductMetadataService | null = null;
|
|
96
|
-
|
|
97
|
-
export function configureProductMetadataService(
|
|
98
|
-
config: ProductMetadataConfig
|
|
99
|
-
): void {
|
|
100
|
-
defaultService = new ProductMetadataService(config);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
export function getProductMetadataService(): ProductMetadataService {
|
|
104
|
-
if (!defaultService) {
|
|
105
|
-
throw new Error(
|
|
106
|
-
"ProductMetadataService not configured. Call configureProductMetadataService first."
|
|
107
|
-
);
|
|
108
|
-
}
|
|
109
|
-
return defaultService;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
export function resetProductMetadataService(): void {
|
|
113
|
-
defaultService = null;
|
|
114
|
-
}
|