@umituz/react-native-subscription 2.31.9 → 2.31.10
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/CreditsRepository.ts +2 -2
- package/src/domains/credits/presentation/useCredits.ts +4 -2
- package/src/domains/subscription/infrastructure/hooks/useSubscriptionPackages.ts +5 -1
- package/src/domains/subscription/infrastructure/services/OfferingsFetcher.ts +2 -1
- package/src/domains/subscription/infrastructure/services/PurchaseHandler.ts +7 -0
- package/src/domains/subscription/infrastructure/services/RevenueCatService.types.ts +2 -2
- package/src/domains/subscription/infrastructure/utils/InitializationCache.ts +14 -8
- package/src/domains/subscription/presentation/useSubscriptionStatus.ts +15 -4
- package/src/domains/trial/infrastructure/DeviceTrialRepository.ts +24 -10
- package/src/domains/wallet/presentation/hooks/useProductMetadata.ts +5 -0
- package/src/domains/wallet/presentation/hooks/useTransactionHistory.ts +5 -0
- package/src/shared/infrastructure/SubscriptionEventBus.ts +2 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-subscription",
|
|
3
|
-
"version": "2.31.
|
|
3
|
+
"version": "2.31.10",
|
|
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,5 +1,5 @@
|
|
|
1
1
|
import { getDoc, setDoc } from "firebase/firestore";
|
|
2
|
-
import { BaseRepository, type Firestore, type DocumentReference } from "@umituz/react-native-firebase";
|
|
2
|
+
import { BaseRepository, serverTimestamp, type Firestore, type DocumentReference } from "@umituz/react-native-firebase";
|
|
3
3
|
import type { CreditsConfig, CreditsResult, DeductCreditsResult } from "../core/Credits";
|
|
4
4
|
import type { UserCreditsDocumentRead, PurchaseSource } from "../core/UserCreditsDocument";
|
|
5
5
|
import { initializeCreditsTransaction } from "../application/CreditsInitializer";
|
|
@@ -146,7 +146,7 @@ export class CreditsRepository extends BaseRepository {
|
|
|
146
146
|
isPremium: false,
|
|
147
147
|
status: SUBSCRIPTION_STATUS.EXPIRED,
|
|
148
148
|
willRenew: false,
|
|
149
|
-
expirationDate:
|
|
149
|
+
expirationDate: serverTimestamp(),
|
|
150
150
|
}, { merge: true });
|
|
151
151
|
}
|
|
152
152
|
}
|
|
@@ -44,9 +44,11 @@ export const useCredits = (): UseCreditsResult => {
|
|
|
44
44
|
return result.data || null;
|
|
45
45
|
},
|
|
46
46
|
enabled: queryEnabled,
|
|
47
|
+
gcTime: 0,
|
|
48
|
+
staleTime: 0,
|
|
47
49
|
refetchOnMount: "always",
|
|
48
|
-
refetchOnWindowFocus:
|
|
49
|
-
refetchOnReconnect:
|
|
50
|
+
refetchOnWindowFocus: "always",
|
|
51
|
+
refetchOnReconnect: "always",
|
|
50
52
|
});
|
|
51
53
|
|
|
52
54
|
const queryClient = useQueryClient();
|
|
@@ -40,6 +40,10 @@ export const useSubscriptionPackages = () => {
|
|
|
40
40
|
return SubscriptionManager.getPackages();
|
|
41
41
|
},
|
|
42
42
|
enabled: isConfigured,
|
|
43
|
-
|
|
43
|
+
gcTime: 0,
|
|
44
|
+
staleTime: 0,
|
|
45
|
+
refetchOnMount: "always",
|
|
46
|
+
refetchOnWindowFocus: "always",
|
|
47
|
+
refetchOnReconnect: "always",
|
|
44
48
|
});
|
|
45
49
|
};
|
|
@@ -9,7 +9,8 @@ export async function fetchOfferings(deps: OfferingsFetcherDeps): Promise<Purcha
|
|
|
9
9
|
try {
|
|
10
10
|
const offerings = await Purchases.getOfferings();
|
|
11
11
|
return offerings.current;
|
|
12
|
-
} catch {
|
|
12
|
+
} catch (error) {
|
|
13
|
+
console.error('[OfferingsFetcher] Failed to fetch offerings', { error });
|
|
13
14
|
return null;
|
|
14
15
|
}
|
|
15
16
|
}
|
|
@@ -125,6 +125,13 @@ export async function handlePurchase(
|
|
|
125
125
|
? `${errorMessage} (Code: ${errorCode})`
|
|
126
126
|
: errorMessage;
|
|
127
127
|
|
|
128
|
+
console.error('[PurchaseHandler] Purchase failed', {
|
|
129
|
+
productId: pkg.product.identifier,
|
|
130
|
+
userId,
|
|
131
|
+
errorCode,
|
|
132
|
+
error,
|
|
133
|
+
});
|
|
134
|
+
|
|
128
135
|
throw new RevenueCatPurchaseError(
|
|
129
136
|
enhancedMessage,
|
|
130
137
|
pkg.product.identifier,
|
|
@@ -80,8 +80,8 @@ export class RevenueCatService implements IRevenueCatService {
|
|
|
80
80
|
try {
|
|
81
81
|
await Purchases.logOut();
|
|
82
82
|
this.stateManager.setInitialized(false);
|
|
83
|
-
} catch {
|
|
84
|
-
|
|
83
|
+
} catch (error) {
|
|
84
|
+
console.error('[RevenueCatService] Logout failed during reset', { error });
|
|
85
85
|
}
|
|
86
86
|
}
|
|
87
87
|
}
|
|
@@ -40,35 +40,41 @@ export class InitializationCache {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
// If we reach here, initialization is in progress for a different user
|
|
43
|
-
//
|
|
44
|
-
return { shouldInit: false, existingPromise:
|
|
43
|
+
// Don't return another user's promise - caller should retry
|
|
44
|
+
return { shouldInit: false, existingPromise: null };
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
setPromise(promise: Promise<boolean>, userId: string): void {
|
|
48
48
|
this.initPromise = promise;
|
|
49
49
|
this.promiseUserId = userId;
|
|
50
50
|
|
|
51
|
+
// Capture userId to prevent stale reference after catch clears promiseUserId
|
|
52
|
+
const targetUserId = userId;
|
|
53
|
+
|
|
51
54
|
// Chain to mark completion and set currentUserId only on success
|
|
52
55
|
promise
|
|
53
56
|
.then((result) => {
|
|
54
|
-
if (result && this.promiseUserId ===
|
|
55
|
-
this.currentUserId =
|
|
57
|
+
if (result && this.promiseUserId === targetUserId) {
|
|
58
|
+
this.currentUserId = targetUserId;
|
|
56
59
|
}
|
|
57
60
|
this.promiseCompleted = true;
|
|
58
61
|
return result;
|
|
59
62
|
})
|
|
60
|
-
.catch(() => {
|
|
63
|
+
.catch((error) => {
|
|
61
64
|
// On failure, clear the promise so retry is possible
|
|
62
|
-
if (this.promiseUserId ===
|
|
65
|
+
if (this.promiseUserId === targetUserId) {
|
|
63
66
|
this.initPromise = null;
|
|
64
67
|
this.promiseUserId = null;
|
|
65
|
-
this.currentUserId = null;
|
|
68
|
+
this.currentUserId = null;
|
|
66
69
|
}
|
|
67
70
|
this.promiseCompleted = true;
|
|
71
|
+
console.error('[InitializationCache] Initialization failed', { userId: targetUserId, error });
|
|
72
|
+
// Re-throw so callers awaiting the promise see the error
|
|
73
|
+
throw error;
|
|
68
74
|
})
|
|
69
75
|
.finally(() => {
|
|
70
76
|
// Always release the mutex
|
|
71
|
-
if (this.promiseUserId ===
|
|
77
|
+
if (this.promiseUserId === targetUserId) {
|
|
72
78
|
this.initializationInProgress = false;
|
|
73
79
|
}
|
|
74
80
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useQuery, useQueryClient } from "@umituz/react-native-design-system";
|
|
2
|
-
import { useEffect } from "react";
|
|
2
|
+
import { useEffect, useRef } from "react";
|
|
3
3
|
import { useAuthStore, selectUserId } from "@umituz/react-native-auth";
|
|
4
4
|
import { SubscriptionManager } from "../infrastructure/managers/SubscriptionManager";
|
|
5
5
|
import { subscriptionEventBus, SUBSCRIPTION_EVENTS } from "../../../shared/infrastructure/SubscriptionEventBus";
|
|
@@ -33,13 +33,24 @@ export const useSubscriptionStatus = (): SubscriptionStatusResult => {
|
|
|
33
33
|
}
|
|
34
34
|
},
|
|
35
35
|
enabled: queryEnabled,
|
|
36
|
+
gcTime: 0,
|
|
37
|
+
staleTime: 0,
|
|
38
|
+
refetchOnMount: "always",
|
|
39
|
+
refetchOnWindowFocus: "always",
|
|
40
|
+
refetchOnReconnect: "always",
|
|
36
41
|
});
|
|
37
42
|
|
|
38
|
-
//
|
|
43
|
+
// Track previous userId to clear stale cache on logout/user switch
|
|
44
|
+
const prevUserIdRef = useRef(userId);
|
|
45
|
+
|
|
39
46
|
useEffect(() => {
|
|
40
|
-
|
|
47
|
+
const prevUserId = prevUserIdRef.current;
|
|
48
|
+
prevUserIdRef.current = userId;
|
|
49
|
+
|
|
50
|
+
// Clear previous user's cache when userId changes (logout or user switch)
|
|
51
|
+
if (prevUserId !== userId && isAuthenticated(prevUserId)) {
|
|
41
52
|
queryClient.removeQueries({
|
|
42
|
-
queryKey: subscriptionStatusQueryKeys.user(
|
|
53
|
+
queryKey: subscriptionStatusQueryKeys.user(prevUserId),
|
|
43
54
|
});
|
|
44
55
|
}
|
|
45
56
|
}, [userId, queryClient]);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { doc
|
|
2
|
-
import { getFirestore, serverTimestamp, type Firestore } from "@umituz/react-native-firebase";
|
|
1
|
+
import { doc } from "firebase/firestore";
|
|
2
|
+
import { getFirestore, runTransaction, serverTimestamp, type Firestore, type Transaction } from "@umituz/react-native-firebase";
|
|
3
3
|
import type { DeviceTrialRecord } from "../core/TrialTypes";
|
|
4
4
|
|
|
5
5
|
const DEVICE_TRIALS_COLLECTION = "device_trials";
|
|
@@ -11,20 +11,34 @@ export class DeviceTrialRepository {
|
|
|
11
11
|
|
|
12
12
|
async getRecord(deviceId: string): Promise<DeviceTrialRecord | null> {
|
|
13
13
|
if (!this.db) return null;
|
|
14
|
-
const
|
|
14
|
+
const ref = doc(this.db, DEVICE_TRIALS_COLLECTION, deviceId);
|
|
15
|
+
const snap = await runTransaction(async (tx: Transaction) => {
|
|
16
|
+
return tx.get(ref);
|
|
17
|
+
});
|
|
15
18
|
return snap.exists() ? snap.data() as DeviceTrialRecord : null;
|
|
16
19
|
}
|
|
17
20
|
|
|
18
21
|
async saveRecord(deviceId: string, data: Partial<DeviceTrialRecord>): Promise<boolean> {
|
|
19
22
|
if (!this.db) return false;
|
|
20
23
|
const ref = doc(this.db, DEVICE_TRIALS_COLLECTION, deviceId);
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
|
|
25
|
+
// Atomic check-then-act: ensure createdAt is set only once
|
|
26
|
+
await runTransaction(async (tx: Transaction) => {
|
|
27
|
+
const snap = await tx.get(ref);
|
|
28
|
+
const existingData = snap.data();
|
|
29
|
+
|
|
30
|
+
const updateData: Record<string, unknown> = {
|
|
31
|
+
...data,
|
|
32
|
+
updatedAt: serverTimestamp(),
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
if (!existingData?.createdAt) {
|
|
36
|
+
updateData.createdAt = serverTimestamp();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
tx.set(ref, updateData, { merge: true });
|
|
40
|
+
});
|
|
41
|
+
|
|
28
42
|
return true;
|
|
29
43
|
}
|
|
30
44
|
}
|
|
@@ -53,6 +53,11 @@ export function useTransactionHistory({
|
|
|
53
53
|
return result.data ?? [];
|
|
54
54
|
},
|
|
55
55
|
enabled: !!userId,
|
|
56
|
+
gcTime: 0,
|
|
57
|
+
staleTime: 0,
|
|
58
|
+
refetchOnMount: "always",
|
|
59
|
+
refetchOnWindowFocus: "always",
|
|
60
|
+
refetchOnReconnect: "always",
|
|
56
61
|
});
|
|
57
62
|
|
|
58
63
|
const transactions = data ?? [];
|
|
@@ -42,8 +42,8 @@ export class SubscriptionEventBus {
|
|
|
42
42
|
this.listeners[event].forEach(callback => {
|
|
43
43
|
try {
|
|
44
44
|
callback(data);
|
|
45
|
-
} catch {
|
|
46
|
-
|
|
45
|
+
} catch (error) {
|
|
46
|
+
console.error('[SubscriptionEventBus] Listener error for event:', event, { error });
|
|
47
47
|
}
|
|
48
48
|
});
|
|
49
49
|
}
|