@umituz/react-native-subscription 2.24.15 → 2.24.17
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 +3 -3
- package/src/domains/paywall/components/PaywallTabBar.tsx +0 -1
- package/src/infrastructure/services/CreditsInitializer.ts +12 -2
- package/src/infrastructure/services/SubscriptionInitializer.ts +47 -2
- package/src/presentation/components/feedback/PaywallFeedbackModal.tsx +2 -9
- package/src/presentation/hooks/useSubscriptionSettingsConfig.ts +2 -2
- package/src/presentation/hooks/useSubscriptionSettingsConfig.utils.ts +30 -4
- package/src/revenuecat/domain/value-objects/RevenueCatConfig.ts +2 -1
- package/src/revenuecat/infrastructure/utils/PremiumStatusSyncer.ts +3 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-subscription",
|
|
3
|
-
"version": "2.24.
|
|
3
|
+
"version": "2.24.17",
|
|
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",
|
|
@@ -56,8 +56,8 @@
|
|
|
56
56
|
"@types/react": "~19.1.10",
|
|
57
57
|
"@typescript-eslint/eslint-plugin": "^8.50.1",
|
|
58
58
|
"@typescript-eslint/parser": "^8.50.1",
|
|
59
|
-
"@umituz/react-native-auth": "
|
|
60
|
-
"@umituz/react-native-design-system": "^2.9.
|
|
59
|
+
"@umituz/react-native-auth": "^3.6.14",
|
|
60
|
+
"@umituz/react-native-design-system": "^2.9.35",
|
|
61
61
|
"@umituz/react-native-firebase": "*",
|
|
62
62
|
"@umituz/react-native-localization": "^3.6.0",
|
|
63
63
|
"eslint": "^9.39.2",
|
|
@@ -18,7 +18,6 @@ interface PaywallTabBarProps {
|
|
|
18
18
|
export const PaywallTabBar: React.FC<PaywallTabBarProps> = React.memo(
|
|
19
19
|
({ activeTab, onTabChange, creditsLabel, subscriptionLabel }) => {
|
|
20
20
|
const tokens = useAppDesignTokens();
|
|
21
|
-
const isCreditsActive = activeTab === "credits";
|
|
22
21
|
|
|
23
22
|
const renderTab = (tab: PaywallTabType, label: string) => {
|
|
24
23
|
const isActive = activeTab === tab;
|
|
@@ -122,9 +122,19 @@ export async function initializeCreditsTransaction(
|
|
|
122
122
|
? [...(existing?.purchaseHistory || []), purchaseMetadata].slice(-10)
|
|
123
123
|
: existing?.purchaseHistory;
|
|
124
124
|
|
|
125
|
-
// Determine subscription status
|
|
125
|
+
// Determine subscription status based on isPremium and willRenew
|
|
126
126
|
const isPremium = metadata?.isPremium ?? true;
|
|
127
|
-
const
|
|
127
|
+
const willRenew = metadata?.willRenew;
|
|
128
|
+
|
|
129
|
+
// Status logic: canceled if premium but willRenew=false, expired if not premium, active otherwise
|
|
130
|
+
let status: SubscriptionDocStatus;
|
|
131
|
+
if (!isPremium) {
|
|
132
|
+
status = "expired";
|
|
133
|
+
} else if (willRenew === false) {
|
|
134
|
+
status = "canceled";
|
|
135
|
+
} else {
|
|
136
|
+
status = "active";
|
|
137
|
+
}
|
|
128
138
|
|
|
129
139
|
// Build credits data (Single Source of Truth)
|
|
130
140
|
const creditsData: Record<string, unknown> = {
|
|
@@ -104,9 +104,54 @@ export const initializeSubscription = async (config: SubscriptionInitConfig): Pr
|
|
|
104
104
|
}
|
|
105
105
|
};
|
|
106
106
|
|
|
107
|
+
/** Sync premium status changes (including cancellation) to Firestore */
|
|
108
|
+
const onPremiumStatusChanged = async (
|
|
109
|
+
userId: string,
|
|
110
|
+
isPremium: boolean,
|
|
111
|
+
productId?: string,
|
|
112
|
+
expiresAt?: string,
|
|
113
|
+
willRenew?: boolean
|
|
114
|
+
) => {
|
|
115
|
+
if (__DEV__) {
|
|
116
|
+
console.log('[SubscriptionInitializer] onPremiumStatusChanged:', { userId, isPremium, productId, willRenew });
|
|
117
|
+
}
|
|
118
|
+
try {
|
|
119
|
+
const revenueCatData: RevenueCatData = {
|
|
120
|
+
expirationDate: expiresAt ?? null,
|
|
121
|
+
willRenew: willRenew ?? false,
|
|
122
|
+
isPremium,
|
|
123
|
+
};
|
|
124
|
+
await getCreditsRepository().initializeCredits(
|
|
125
|
+
userId,
|
|
126
|
+
`status_sync_${Date.now()}`,
|
|
127
|
+
productId,
|
|
128
|
+
"settings" as any,
|
|
129
|
+
revenueCatData
|
|
130
|
+
);
|
|
131
|
+
if (__DEV__) {
|
|
132
|
+
console.log('[SubscriptionInitializer] Premium status synced to Firestore');
|
|
133
|
+
}
|
|
134
|
+
onCreditsUpdated?.(userId);
|
|
135
|
+
} catch (error) {
|
|
136
|
+
if (__DEV__) {
|
|
137
|
+
console.error('[SubscriptionInitializer] Premium status sync failed:', error);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
|
|
107
142
|
SubscriptionManager.configure({
|
|
108
|
-
config: {
|
|
109
|
-
|
|
143
|
+
config: {
|
|
144
|
+
apiKey: key,
|
|
145
|
+
testStoreKey,
|
|
146
|
+
entitlementIdentifier: entitlementId,
|
|
147
|
+
consumableProductIdentifiers: [creditPackages?.identifierPattern || "credit"],
|
|
148
|
+
onPurchaseCompleted: onPurchase,
|
|
149
|
+
onRenewalDetected: onRenewal,
|
|
150
|
+
onPremiumStatusChanged,
|
|
151
|
+
onCreditsUpdated,
|
|
152
|
+
},
|
|
153
|
+
apiKey: key,
|
|
154
|
+
getAnonymousUserId,
|
|
110
155
|
});
|
|
111
156
|
|
|
112
157
|
const userId = await waitForAuthState(getFirebaseAuth, authStateTimeoutMs);
|
|
@@ -4,14 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import React, { useMemo } from "react";
|
|
7
|
-
import {
|
|
8
|
-
View,
|
|
9
|
-
Modal,
|
|
10
|
-
TouchableOpacity,
|
|
11
|
-
Pressable,
|
|
12
|
-
TextInput,
|
|
13
|
-
KeyboardAvoidingView,
|
|
14
|
-
} from "react-native";
|
|
7
|
+
import { View, TouchableOpacity, TextInput } from "react-native";
|
|
15
8
|
import { AtomicText, BaseModal } from "@umituz/react-native-design-system";
|
|
16
9
|
import { useAppDesignTokens } from "@umituz/react-native-design-system";
|
|
17
10
|
import { useLocalization } from "@umituz/react-native-localization";
|
|
@@ -78,7 +71,7 @@ export const PaywallFeedbackModal: React.FC<PaywallFeedbackModalProps> = React.m
|
|
|
78
71
|
>
|
|
79
72
|
<View style={{ paddingHorizontal: tokens.spacing.md, paddingBottom: tokens.spacing.lg }}>
|
|
80
73
|
<View style={[styles.optionsContainer, { backgroundColor: 'transparent', padding: 0 }]}>
|
|
81
|
-
{FEEDBACK_OPTION_IDS.map((optionId
|
|
74
|
+
{FEEDBACK_OPTION_IDS.map((optionId) => {
|
|
82
75
|
const isSelected = selectedReason === optionId;
|
|
83
76
|
const isOther = optionId === "other";
|
|
84
77
|
const showInput = isSelected && isOther;
|
|
@@ -65,10 +65,10 @@ export const useSubscriptionSettingsConfig = (
|
|
|
65
65
|
// Days remaining
|
|
66
66
|
const daysRemaining = useMemo(() => calculateDaysRemaining(expiresAtIso), [expiresAtIso]);
|
|
67
67
|
|
|
68
|
-
// Status type
|
|
68
|
+
// Status type: prioritize Firestore status, then derive from willRenew + expiration
|
|
69
69
|
const statusType: SubscriptionStatusType = credits?.status
|
|
70
70
|
? (credits.status as SubscriptionStatusType)
|
|
71
|
-
: getSubscriptionStatusType(isPremium);
|
|
71
|
+
: getSubscriptionStatusType(isPremium, willRenew, expiresAtIso);
|
|
72
72
|
|
|
73
73
|
const creditsArray = useCreditsArray(credits, dynamicCreditLimit, translations);
|
|
74
74
|
|
|
@@ -37,10 +37,36 @@ export function useCreditsArray(
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
/**
|
|
40
|
-
* Calculates subscription status type
|
|
40
|
+
* Calculates subscription status type based on premium and renewal status
|
|
41
|
+
* @param isPremium - Whether user has premium subscription
|
|
42
|
+
* @param willRenew - Whether subscription will auto-renew (false = canceled)
|
|
43
|
+
* @param expiresAt - Expiration date ISO string (null for lifetime)
|
|
41
44
|
*/
|
|
42
45
|
export function getSubscriptionStatusType(
|
|
43
|
-
isPremium: boolean
|
|
44
|
-
|
|
45
|
-
|
|
46
|
+
isPremium: boolean,
|
|
47
|
+
willRenew?: boolean,
|
|
48
|
+
expiresAt?: string | null
|
|
49
|
+
): "active" | "canceled" | "expired" | "none" {
|
|
50
|
+
if (!isPremium) {
|
|
51
|
+
return "none";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Lifetime subscription (no expiration) - always active
|
|
55
|
+
if (!expiresAt) {
|
|
56
|
+
return "active";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Check if expired
|
|
60
|
+
const now = new Date();
|
|
61
|
+
const expDate = new Date(expiresAt);
|
|
62
|
+
if (expDate < now) {
|
|
63
|
+
return "expired";
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Premium with willRenew=false means subscription is canceled but still active until expiration
|
|
67
|
+
if (willRenew === false) {
|
|
68
|
+
return "canceled";
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return "active";
|
|
46
72
|
}
|
|
@@ -19,7 +19,8 @@ export interface RevenueCatConfig {
|
|
|
19
19
|
userId: string,
|
|
20
20
|
isPremium: boolean,
|
|
21
21
|
productId?: string,
|
|
22
|
-
expiresAt?: string
|
|
22
|
+
expiresAt?: string,
|
|
23
|
+
willRenew?: boolean
|
|
23
24
|
) => Promise<void> | void;
|
|
24
25
|
/** Callback for purchase completion */
|
|
25
26
|
onPurchaseCompleted?: (
|
|
@@ -27,10 +27,11 @@ export async function syncPremiumStatus(
|
|
|
27
27
|
userId,
|
|
28
28
|
true,
|
|
29
29
|
premiumEntitlement.productIdentifier,
|
|
30
|
-
premiumEntitlement.expirationDate ?? undefined
|
|
30
|
+
premiumEntitlement.expirationDate ?? undefined,
|
|
31
|
+
premiumEntitlement.willRenew
|
|
31
32
|
);
|
|
32
33
|
} else {
|
|
33
|
-
await config.onPremiumStatusChanged(userId, false);
|
|
34
|
+
await config.onPremiumStatusChanged(userId, false, undefined, undefined, undefined);
|
|
34
35
|
}
|
|
35
36
|
} catch {
|
|
36
37
|
// Silent error handling
|