@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-subscription",
3
- "version": "2.24.15",
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.0",
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 status: SubscriptionDocStatus = isPremium ? "active" : "expired";
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: { apiKey: key, testStoreKey, entitlementIdentifier: entitlementId, consumableProductIdentifiers: [creditPackages?.identifierPattern || "credit"], onPurchaseCompleted: onPurchase, onRenewalDetected: onRenewal, onCreditsUpdated },
109
- apiKey: key, getAnonymousUserId
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, index) => {
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 from Firestore or derived
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
- ): "active" | "none" {
45
- return isPremium ? "active" : "none";
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