@umituz/react-native-subscription 2.35.14 → 2.35.16
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/config/utils/planSelectors.ts +5 -1
- package/src/domains/credits/presentation/useCredits.ts +21 -6
- package/src/domains/subscription/application/SubscriptionSyncProcessor.ts +1 -1
- package/src/domains/subscription/application/initializer/ConfigValidator.ts +2 -2
- package/src/domains/subscription/application/initializer/ServiceConfigurator.ts +23 -3
- package/src/domains/subscription/infrastructure/hooks/customer-info/useCustomerInfo.ts +1 -1
- package/src/domains/subscription/infrastructure/hooks/usePaywallFlow.ts +12 -2
- package/src/domains/subscription/infrastructure/hooks/useRevenueCat.ts +23 -5
- package/src/domains/subscription/presentation/useFeatureGate.ts +1 -1
- package/src/domains/trial/application/TrialEligibilityService.ts +1 -1
- package/src/domains/trial/application/TrialService.ts +12 -4
- package/src/domains/wallet/infrastructure/repositories/transaction/TransactionWriter.ts +1 -1
- package/src/domains/wallet/presentation/hooks/useProductMetadata.ts +1 -1
- package/src/domains/wallet/presentation/hooks/useTransactionHistory.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-subscription",
|
|
3
|
-
"version": "2.35.
|
|
3
|
+
"version": "2.35.16",
|
|
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",
|
|
@@ -32,7 +32,11 @@ export const getCreditLimitForPlan = (
|
|
|
32
32
|
planId: string
|
|
33
33
|
): number => {
|
|
34
34
|
const plan = getPlanById(config, planId);
|
|
35
|
-
|
|
35
|
+
if (!plan) {
|
|
36
|
+
console.warn(`[planSelectors] Plan not found: ${planId}, returning 0`);
|
|
37
|
+
return 0;
|
|
38
|
+
}
|
|
39
|
+
return plan.credits;
|
|
36
40
|
};
|
|
37
41
|
|
|
38
42
|
export const determinePlanFromCredits = (
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useQuery, useQueryClient } from "@umituz/react-native-design-system";
|
|
2
|
-
import { useCallback, useMemo, useEffect } from "react";
|
|
2
|
+
import { useCallback, useMemo, useEffect, useRef } from "react";
|
|
3
3
|
import { useAuthStore, selectUserId } from "@umituz/react-native-auth";
|
|
4
4
|
import { subscriptionEventBus, SUBSCRIPTION_EVENTS } from "../../../shared/infrastructure/SubscriptionEventBus";
|
|
5
5
|
import {
|
|
@@ -44,15 +44,30 @@ export const useCredits = (): UseCreditsResult => {
|
|
|
44
44
|
return result.data ?? null;
|
|
45
45
|
},
|
|
46
46
|
enabled: queryEnabled,
|
|
47
|
-
gcTime:
|
|
48
|
-
staleTime:
|
|
49
|
-
refetchOnMount:
|
|
50
|
-
refetchOnWindowFocus:
|
|
51
|
-
refetchOnReconnect:
|
|
47
|
+
gcTime: 0,
|
|
48
|
+
staleTime: 0,
|
|
49
|
+
refetchOnMount: "always",
|
|
50
|
+
refetchOnWindowFocus: "always",
|
|
51
|
+
refetchOnReconnect: "always",
|
|
52
52
|
});
|
|
53
53
|
|
|
54
54
|
const queryClient = useQueryClient();
|
|
55
55
|
|
|
56
|
+
// Track previous userId to clear stale cache on logout/user switch
|
|
57
|
+
const prevUserIdRef = useRef(userId);
|
|
58
|
+
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
const prevUserId = prevUserIdRef.current;
|
|
61
|
+
prevUserIdRef.current = userId;
|
|
62
|
+
|
|
63
|
+
// Clear previous user's cache when userId changes (logout or user switch)
|
|
64
|
+
if (prevUserId !== userId && isAuthenticated(prevUserId)) {
|
|
65
|
+
queryClient.removeQueries({
|
|
66
|
+
queryKey: creditsQueryKeys.user(prevUserId),
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}, [userId, queryClient]);
|
|
70
|
+
|
|
56
71
|
useEffect(() => {
|
|
57
72
|
if (!isAuthenticated(userId)) return undefined;
|
|
58
73
|
|
|
@@ -43,7 +43,7 @@ export class SubscriptionSyncProcessor {
|
|
|
43
43
|
|
|
44
44
|
async processRenewal(userId: string, productId: string, newExpirationDate: string, customerInfo: CustomerInfo) {
|
|
45
45
|
const revenueCatData = extractRevenueCatData(customerInfo, this.entitlementId);
|
|
46
|
-
revenueCatData.expirationDate = newExpirationDate
|
|
46
|
+
revenueCatData.expirationDate = newExpirationDate ?? revenueCatData.expirationDate;
|
|
47
47
|
const purchaseId = generateRenewalId(revenueCatData.originalTransactionId, productId, newExpirationDate);
|
|
48
48
|
|
|
49
49
|
const creditsUserId = await this.getCreditsUserId(userId);
|
|
@@ -4,8 +4,8 @@ import type { SubscriptionInitConfig } from "../SubscriptionInitializerTypes";
|
|
|
4
4
|
export function getApiKey(config: SubscriptionInitConfig): string {
|
|
5
5
|
const { apiKey, apiKeyIos, apiKeyAndroid } = config;
|
|
6
6
|
const key = Platform.OS === 'ios'
|
|
7
|
-
? (apiKeyIos
|
|
8
|
-
: (apiKeyAndroid
|
|
7
|
+
? (apiKeyIos ?? apiKey)
|
|
8
|
+
: (apiKeyAndroid ?? apiKey);
|
|
9
9
|
|
|
10
10
|
if (!key) {
|
|
11
11
|
throw new Error('API key required');
|
|
@@ -3,6 +3,8 @@ import { SubscriptionManager } from "../../infrastructure/managers/SubscriptionM
|
|
|
3
3
|
import { configureAuthProvider } from "../../presentation/useAuthAwarePurchase";
|
|
4
4
|
import { SubscriptionSyncService } from "../SubscriptionSyncService";
|
|
5
5
|
import type { SubscriptionInitConfig } from "../SubscriptionInitializerTypes";
|
|
6
|
+
import type { CustomerInfo } from "react-native-purchases";
|
|
7
|
+
import type { PackageType } from "../../../revenuecat/core/types/RevenueCatTypes";
|
|
6
8
|
|
|
7
9
|
export function configureServices(config: SubscriptionInitConfig, apiKey: string): SubscriptionSyncService {
|
|
8
10
|
const { entitlementId, credits, creditPackages, getFirebaseAuth, showAuthModal, onCreditsUpdated, getAnonymousUserId } = config;
|
|
@@ -23,9 +25,27 @@ export function configureServices(config: SubscriptionInitConfig, apiKey: string
|
|
|
23
25
|
apiKey,
|
|
24
26
|
entitlementIdentifier: entitlementId,
|
|
25
27
|
consumableProductIdentifiers: [creditPackages.identifierPattern],
|
|
26
|
-
onPurchaseCompleted: (
|
|
27
|
-
|
|
28
|
-
|
|
28
|
+
onPurchaseCompleted: (
|
|
29
|
+
u: string,
|
|
30
|
+
p: string,
|
|
31
|
+
c: CustomerInfo,
|
|
32
|
+
s?: string,
|
|
33
|
+
pkgType?: PackageType | null
|
|
34
|
+
) => syncService.handlePurchase(u, p, c, s as any, pkgType),
|
|
35
|
+
onRenewalDetected: (
|
|
36
|
+
u: string,
|
|
37
|
+
p: string,
|
|
38
|
+
expires: string,
|
|
39
|
+
c: CustomerInfo
|
|
40
|
+
) => syncService.handleRenewal(u, p, expires, c),
|
|
41
|
+
onPremiumStatusChanged: (
|
|
42
|
+
u: string,
|
|
43
|
+
isP: boolean,
|
|
44
|
+
pId?: string,
|
|
45
|
+
exp?: string,
|
|
46
|
+
willR?: boolean,
|
|
47
|
+
pt?: string
|
|
48
|
+
) => syncService.handlePremiumStatusChanged(u, isP, pId, exp, willR, pt as any),
|
|
29
49
|
onCreditsUpdated,
|
|
30
50
|
},
|
|
31
51
|
apiKey,
|
|
@@ -29,17 +29,27 @@ export const usePaywallFlow = (options: UsePaywallFlowOptions = {}): UsePaywallF
|
|
|
29
29
|
|
|
30
30
|
// Load persisted state
|
|
31
31
|
useEffect(() => {
|
|
32
|
+
let isMounted = true;
|
|
33
|
+
|
|
32
34
|
const loadPersistedState = async () => {
|
|
33
35
|
try {
|
|
34
36
|
const value = await getString(PAYWALL_SHOWN_KEY, '');
|
|
35
|
-
|
|
37
|
+
if (isMounted) {
|
|
38
|
+
setPaywallShown(value === 'true');
|
|
39
|
+
}
|
|
36
40
|
} catch (error) {
|
|
37
41
|
console.error('[usePaywallFlow] Failed to load paywall state', error);
|
|
38
|
-
|
|
42
|
+
if (isMounted) {
|
|
43
|
+
setPaywallShown(false); // Safe default
|
|
44
|
+
}
|
|
39
45
|
}
|
|
40
46
|
};
|
|
41
47
|
|
|
42
48
|
loadPersistedState();
|
|
49
|
+
|
|
50
|
+
return () => {
|
|
51
|
+
isMounted = false;
|
|
52
|
+
};
|
|
43
53
|
}, [getString]);
|
|
44
54
|
|
|
45
55
|
const closePostOnboardingPaywall = useCallback(async (_isPremium: boolean) => {
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* React hook for RevenueCat subscription management
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { useState, useCallback } from "react";
|
|
6
|
+
import { useState, useCallback, useEffect, useRef } from "react";
|
|
7
7
|
import type { PurchasesOffering, PurchasesPackage } from "react-native-purchases";
|
|
8
8
|
import { getRevenueCatService } from '../../infrastructure/services/RevenueCatService';
|
|
9
9
|
import type { PurchaseResult, RestoreResult } from '../../../../shared/application/ports/IRevenueCatService';
|
|
@@ -35,8 +35,18 @@ export interface UseRevenueCatResult {
|
|
|
35
35
|
export function useRevenueCat(): UseRevenueCatResult {
|
|
36
36
|
const [offering, setOffering] = useState<PurchasesOffering | null>(null);
|
|
37
37
|
const [loading, setLoading] = useState(false);
|
|
38
|
+
const isMountedRef = useRef(true);
|
|
39
|
+
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
isMountedRef.current = true;
|
|
42
|
+
return () => {
|
|
43
|
+
isMountedRef.current = false;
|
|
44
|
+
};
|
|
45
|
+
}, []);
|
|
38
46
|
|
|
39
47
|
const initialize = useCallback(async (userId: string, apiKey?: string) => {
|
|
48
|
+
if (!isMountedRef.current) return;
|
|
49
|
+
|
|
40
50
|
setLoading(true);
|
|
41
51
|
try {
|
|
42
52
|
const service = getRevenueCatService();
|
|
@@ -44,17 +54,21 @@ export function useRevenueCat(): UseRevenueCatResult {
|
|
|
44
54
|
return;
|
|
45
55
|
}
|
|
46
56
|
const result = await service.initialize(userId, apiKey);
|
|
47
|
-
if (result.success) {
|
|
57
|
+
if (result.success && isMountedRef.current) {
|
|
48
58
|
setOffering(result.offering);
|
|
49
59
|
}
|
|
50
60
|
} catch {
|
|
51
61
|
// Error handling is done by service
|
|
52
62
|
} finally {
|
|
53
|
-
|
|
63
|
+
if (isMountedRef.current) {
|
|
64
|
+
setLoading(false);
|
|
65
|
+
}
|
|
54
66
|
}
|
|
55
67
|
}, []);
|
|
56
68
|
|
|
57
69
|
const loadOfferings = useCallback(async () => {
|
|
70
|
+
if (!isMountedRef.current) return;
|
|
71
|
+
|
|
58
72
|
setLoading(true);
|
|
59
73
|
try {
|
|
60
74
|
const service = getRevenueCatService();
|
|
@@ -62,11 +76,15 @@ export function useRevenueCat(): UseRevenueCatResult {
|
|
|
62
76
|
return;
|
|
63
77
|
}
|
|
64
78
|
const fetchedOffering = await service.fetchOfferings();
|
|
65
|
-
|
|
79
|
+
if (isMountedRef.current) {
|
|
80
|
+
setOffering(fetchedOffering);
|
|
81
|
+
}
|
|
66
82
|
} catch {
|
|
67
83
|
// Error handling is done by service
|
|
68
84
|
} finally {
|
|
69
|
-
|
|
85
|
+
if (isMountedRef.current) {
|
|
86
|
+
setLoading(false);
|
|
87
|
+
}
|
|
70
88
|
}
|
|
71
89
|
}, []);
|
|
72
90
|
|
|
@@ -108,7 +108,7 @@ export function useFeatureGate(params: UseFeatureGateParams): UseFeatureGateResu
|
|
|
108
108
|
}
|
|
109
109
|
|
|
110
110
|
prevCreditBalanceRef.current = creditBalance;
|
|
111
|
-
hasSubscriptionRef
|
|
111
|
+
// hasSubscriptionRef is already synced by useSyncedRefs, no need to update manually
|
|
112
112
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
113
113
|
}, [creditBalance, hasSubscription]);
|
|
114
114
|
|
|
@@ -12,7 +12,7 @@ export class TrialEligibilityService {
|
|
|
12
12
|
|
|
13
13
|
const { hasUsedTrial, trialInProgress, userIds = [] } = record;
|
|
14
14
|
|
|
15
|
-
if (userId && userIds.includes(userId)) {
|
|
15
|
+
if (userId && userId.length > 0 && userIds.includes(userId)) {
|
|
16
16
|
return { eligible: false, reason: "user_already_used", deviceId };
|
|
17
17
|
}
|
|
18
18
|
|
|
@@ -26,9 +26,17 @@ const repository = new DeviceTrialRepository();
|
|
|
26
26
|
|
|
27
27
|
export const getDeviceId = () => PersistentDeviceIdService.getDeviceId();
|
|
28
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Ensures a valid device ID is available
|
|
31
|
+
* Uses provided deviceId if non-empty, otherwise fetches from PersistentDeviceIdService
|
|
32
|
+
*/
|
|
33
|
+
async function ensureDeviceId(deviceId?: string): Promise<string> {
|
|
34
|
+
return (deviceId && deviceId.length > 0) ? deviceId : await getDeviceId();
|
|
35
|
+
}
|
|
36
|
+
|
|
29
37
|
export async function checkTrialEligibility(userId?: string, deviceId?: string): Promise<TrialEligibilityResult> {
|
|
30
38
|
try {
|
|
31
|
-
const id =
|
|
39
|
+
const id = await ensureDeviceId(deviceId);
|
|
32
40
|
const record = await repository.getRecord(id);
|
|
33
41
|
return TrialEligibilityService.check(userId, id, record);
|
|
34
42
|
} catch {
|
|
@@ -38,7 +46,7 @@ export async function checkTrialEligibility(userId?: string, deviceId?: string):
|
|
|
38
46
|
|
|
39
47
|
export async function recordTrialStart(userId: string, deviceId?: string): Promise<boolean> {
|
|
40
48
|
try {
|
|
41
|
-
const id =
|
|
49
|
+
const id = await ensureDeviceId(deviceId);
|
|
42
50
|
const record: TrialRecordWrite = {
|
|
43
51
|
deviceId: id,
|
|
44
52
|
trialInProgress: true,
|
|
@@ -54,7 +62,7 @@ export async function recordTrialStart(userId: string, deviceId?: string): Promi
|
|
|
54
62
|
|
|
55
63
|
export async function recordTrialEnd(deviceId?: string): Promise<boolean> {
|
|
56
64
|
try {
|
|
57
|
-
const id =
|
|
65
|
+
const id = await ensureDeviceId(deviceId);
|
|
58
66
|
const record: TrialRecordWrite = {
|
|
59
67
|
hasUsedTrial: true,
|
|
60
68
|
trialInProgress: false,
|
|
@@ -68,7 +76,7 @@ export async function recordTrialEnd(deviceId?: string): Promise<boolean> {
|
|
|
68
76
|
|
|
69
77
|
export async function recordTrialConversion(deviceId?: string): Promise<boolean> {
|
|
70
78
|
try {
|
|
71
|
-
const id =
|
|
79
|
+
const id = await ensureDeviceId(deviceId);
|
|
72
80
|
const record: TrialRecordWrite = {
|
|
73
81
|
hasUsedTrial: true,
|
|
74
82
|
trialInProgress: false,
|