@umituz/react-native-subscription 2.14.15 → 2.14.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.14.15",
3
+ "version": "2.14.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",
@@ -51,7 +51,8 @@
51
51
  "@types/react": "~19.1.10",
52
52
  "@typescript-eslint/eslint-plugin": "^8.50.1",
53
53
  "@typescript-eslint/parser": "^8.50.1",
54
- "@umituz/react-native-auth": "^2.7.3",
54
+ "@umituz/react-native-auth": "*",
55
+ "@umituz/react-native-storage": "*",
55
56
  "@umituz/react-native-design-system": "*",
56
57
  "@umituz/react-native-firebase": "*",
57
58
  "@umituz/react-native-legal": "*",
package/src/index.ts CHANGED
@@ -317,6 +317,13 @@ export {
317
317
  type UsePremiumResult,
318
318
  } from "./presentation/hooks/usePremium";
319
319
 
320
+ export {
321
+ useSubscriptionStatus,
322
+ subscriptionStatusQueryKeys,
323
+ type SubscriptionStatusResult,
324
+ type UseSubscriptionStatusParams,
325
+ } from "./presentation/hooks/useSubscriptionStatus";
326
+
320
327
  export {
321
328
  usePaywallOperations,
322
329
  type UsePaywallOperationsProps,
@@ -2,12 +2,16 @@
2
2
  * usePremium Hook
3
3
  * Complete subscription management for 100+ apps
4
4
  * Works for both authenticated and anonymous users
5
+ *
6
+ * IMPORTANT: isPremium is based on actual RevenueCat subscription status,
7
+ * NOT on whether credits document exists.
5
8
  */
6
9
 
7
10
  import { useCallback } from 'react';
8
11
  import type { PurchasesPackage } from 'react-native-purchases';
9
12
  import type { UserCredits } from '../../domain/entities/Credits';
10
13
  import { useCredits } from './useCredits';
14
+ import { useSubscriptionStatus } from './useSubscriptionStatus';
11
15
  import {
12
16
  useSubscriptionPackages,
13
17
  usePurchasePackage,
@@ -54,6 +58,13 @@ export const usePremium = (userId?: string): UsePremiumResult => {
54
58
  console.log('[DEBUG usePremium] Hook called', { userId: userId || 'ANONYMOUS' });
55
59
  }
56
60
 
61
+ // Fetch real subscription status from RevenueCat
62
+ const { isPremium: subscriptionActive, isLoading: statusLoading } =
63
+ useSubscriptionStatus({
64
+ userId,
65
+ enabled: !!userId,
66
+ });
67
+
57
68
  // Fetch user credits (server state)
58
69
  const { credits, isLoading: creditsLoading } = useCredits({
59
70
  userId,
@@ -70,7 +81,8 @@ export const usePremium = (userId?: string): UsePremiumResult => {
70
81
  packagesCount: packages?.length || 0,
71
82
  packagesLoading,
72
83
  creditsLoading,
73
- isPremium: credits !== null,
84
+ statusLoading,
85
+ isPremium: subscriptionActive,
74
86
  });
75
87
  }
76
88
 
@@ -82,8 +94,8 @@ export const usePremium = (userId?: string): UsePremiumResult => {
82
94
  const { showPaywall, setShowPaywall, closePaywall, openPaywall } =
83
95
  usePaywallVisibility();
84
96
 
85
- // Premium status = has credits
86
- const isPremium = credits !== null;
97
+ // Premium status = actual subscription status from RevenueCat
98
+ const isPremium = subscriptionActive;
87
99
 
88
100
  // Purchase handler with proper error handling
89
101
  const handlePurchase = useCallback(
@@ -117,6 +129,7 @@ export const usePremium = (userId?: string): UsePremiumResult => {
117
129
  return {
118
130
  isPremium,
119
131
  isLoading:
132
+ statusLoading ||
120
133
  creditsLoading ||
121
134
  packagesLoading ||
122
135
  purchaseMutation.isPending ||
@@ -4,9 +4,9 @@
4
4
  * Package-driven: all logic handled internally
5
5
  */
6
6
 
7
- import { useMemo, useCallback } from "react";
8
- import { Linking } from "react-native";
7
+ import { useMemo } from "react";
9
8
  import { useCredits } from "./useCredits";
9
+ import { useSubscriptionStatus } from "./useSubscriptionStatus";
10
10
  import { useCustomerInfo } from "../../revenuecat/presentation/hooks/useCustomerInfo";
11
11
  import { usePaywallVisibility } from "./usePaywallVisibility";
12
12
  import {
@@ -45,11 +45,15 @@ export const useSubscriptionSettingsConfig = (
45
45
 
46
46
  // Internal hooks
47
47
  const { credits } = useCredits({ userId, enabled: !!userId });
48
+ const { isPremium: subscriptionActive } = useSubscriptionStatus({
49
+ userId,
50
+ enabled: !!userId,
51
+ });
48
52
  const { customerInfo } = useCustomerInfo();
49
53
  const { openPaywall } = usePaywallVisibility();
50
54
 
51
- // Premium status from credits
52
- const isPremium = credits !== null;
55
+ // Premium status from actual RevenueCat subscription
56
+ const isPremium = subscriptionActive;
53
57
 
54
58
  // RevenueCat entitlement info
55
59
  const premiumEntitlement = customerInfo?.entitlements.active["premium"];
@@ -74,14 +78,8 @@ export const useSubscriptionSettingsConfig = (
74
78
  [expiresAtIso]
75
79
  );
76
80
 
77
- // Subscription press handler
78
- const handleSubscriptionPress = useCallback(() => {
79
- if (isPremium) {
80
- Linking.openURL("https://apps.apple.com/account/subscriptions");
81
- } else {
82
- openPaywall();
83
- }
84
- }, [isPremium, openPaywall]);
81
+ // Subscription press handler - always opens paywall for upgrade
82
+ const handleSubscriptionPress = openPaywall;
85
83
 
86
84
  // Status type
87
85
  const statusType: SubscriptionStatusType = isPremium ? "active" : "none";
@@ -140,7 +138,6 @@ export const useSubscriptionSettingsConfig = (
140
138
  manageButton: translations.manageButton,
141
139
  upgradeButton: translations.upgradeButton,
142
140
  },
143
- onManageSubscription: handleSubscriptionPress,
144
141
  onUpgrade: openPaywall,
145
142
  },
146
143
  }),
@@ -154,7 +151,6 @@ export const useSubscriptionSettingsConfig = (
154
151
  daysRemaining,
155
152
  willRenew,
156
153
  creditsArray,
157
- handleSubscriptionPress,
158
154
  openPaywall,
159
155
  ]
160
156
  );
@@ -0,0 +1,73 @@
1
+ /**
2
+ * useSubscriptionStatus Hook
3
+ *
4
+ * TanStack Query hook for checking real subscription status from RevenueCat.
5
+ * This provides the actual premium status based on entitlements, not credits.
6
+ */
7
+
8
+ import { useQuery } from "@tanstack/react-query";
9
+ import { SubscriptionManager } from "../../revenuecat/infrastructure/managers/SubscriptionManager";
10
+
11
+ declare const __DEV__: boolean;
12
+
13
+ export const subscriptionStatusQueryKeys = {
14
+ all: ["subscriptionStatus"] as const,
15
+ user: (userId: string) => ["subscriptionStatus", userId] as const,
16
+ };
17
+
18
+ export interface SubscriptionStatusResult {
19
+ isPremium: boolean;
20
+ expirationDate: Date | null;
21
+ isLoading: boolean;
22
+ error: Error | null;
23
+ refetch: () => void;
24
+ }
25
+
26
+ export interface UseSubscriptionStatusParams {
27
+ userId: string | undefined;
28
+ enabled?: boolean;
29
+ }
30
+
31
+ /**
32
+ * Check real subscription status from RevenueCat
33
+ *
34
+ * @param userId - User ID
35
+ * @param enabled - Whether to enable the query
36
+ * @returns Subscription status with isPremium flag
37
+ */
38
+ export const useSubscriptionStatus = ({
39
+ userId,
40
+ enabled = true,
41
+ }: UseSubscriptionStatusParams): SubscriptionStatusResult => {
42
+ const { data, isLoading, error, refetch } = useQuery({
43
+ queryKey: subscriptionStatusQueryKeys.user(userId ?? ""),
44
+ queryFn: async () => {
45
+ if (!userId) {
46
+ return { isPremium: false, expirationDate: null };
47
+ }
48
+
49
+ const status = await SubscriptionManager.checkPremiumStatus();
50
+
51
+ if (__DEV__) {
52
+ console.log("[useSubscriptionStatus] Status from RevenueCat", {
53
+ userId,
54
+ isPremium: status.isPremium,
55
+ expirationDate: status.expirationDate,
56
+ });
57
+ }
58
+
59
+ return status;
60
+ },
61
+ enabled: enabled && !!userId && SubscriptionManager.isInitialized(),
62
+ staleTime: 30 * 1000,
63
+ gcTime: 5 * 60 * 1000,
64
+ });
65
+
66
+ return {
67
+ isPremium: data?.isPremium ?? false,
68
+ expirationDate: data?.expirationDate ?? null,
69
+ isLoading,
70
+ error: error as Error | null,
71
+ refetch,
72
+ };
73
+ };
@@ -4,8 +4,8 @@
4
4
  * No business logic - pure presentation
5
5
  */
6
6
 
7
- import React from "react";
8
- import { StyleSheet } from "react-native";
7
+ import React, { useMemo } from "react";
8
+ import { StyleSheet, View } from "react-native";
9
9
  import { useAppDesignTokens, ScreenLayout } from "@umituz/react-native-design-system";
10
10
  import { SubscriptionHeader } from "./components/SubscriptionHeader";
11
11
  import { CreditsList } from "./components/CreditsList";
@@ -59,6 +59,26 @@ export const SubscriptionDetailScreen: React.FC<
59
59
  > = ({ config }) => {
60
60
  const tokens = useAppDesignTokens();
61
61
  const showCredits = config.credits && config.credits.length > 0;
62
+ const showUpgradeButton = !config.isPremium && config.onUpgrade;
63
+
64
+ const styles = useMemo(
65
+ () =>
66
+ StyleSheet.create({
67
+ content: {
68
+ flexGrow: 1,
69
+ padding: tokens.spacing.lg,
70
+ gap: tokens.spacing.lg,
71
+ },
72
+ cardsContainer: {
73
+ gap: tokens.spacing.lg,
74
+ },
75
+ spacer: {
76
+ flex: 1,
77
+ minHeight: tokens.spacing.xl,
78
+ },
79
+ }),
80
+ [tokens]
81
+ );
62
82
 
63
83
  return (
64
84
  <ScreenLayout
@@ -75,41 +95,38 @@ export const SubscriptionDetailScreen: React.FC<
75
95
  ) : undefined
76
96
  }
77
97
  >
78
- <SubscriptionHeader
79
- statusType={config.statusType}
80
- isPremium={config.isPremium}
81
- isLifetime={config.isLifetime}
82
- expirationDate={config.expirationDate}
83
- purchaseDate={config.purchaseDate}
84
- daysRemaining={config.daysRemaining}
85
- translations={config.translations}
86
- />
98
+ <View style={styles.cardsContainer}>
99
+ <SubscriptionHeader
100
+ statusType={config.statusType}
101
+ isPremium={config.isPremium}
102
+ isLifetime={config.isLifetime}
103
+ expirationDate={config.expirationDate}
104
+ purchaseDate={config.purchaseDate}
105
+ daysRemaining={config.daysRemaining}
106
+ translations={config.translations}
107
+ />
108
+
109
+ {showCredits && (
110
+ <CreditsList
111
+ credits={config.credits!}
112
+ title={
113
+ config.translations.usageTitle || config.translations.creditsTitle
114
+ }
115
+ description={config.translations.creditsResetInfo}
116
+ remainingLabel={config.translations.remainingLabel}
117
+ />
118
+ )}
119
+ </View>
120
+
121
+ <View style={styles.spacer} />
87
122
 
88
- {showCredits && (
89
- <CreditsList
90
- credits={config.credits!}
91
- title={
92
- config.translations.usageTitle || config.translations.creditsTitle
93
- }
94
- description={config.translations.creditsResetInfo}
95
- remainingLabel={config.translations.remainingLabel}
123
+ {showUpgradeButton && (
124
+ <SubscriptionActions
125
+ isPremium={config.isPremium}
126
+ upgradeButtonLabel={config.translations.upgradeButton}
127
+ onUpgrade={config.onUpgrade}
96
128
  />
97
129
  )}
98
-
99
- <SubscriptionActions
100
- isPremium={config.isPremium}
101
- manageButtonLabel={config.translations.manageButton}
102
- upgradeButtonLabel={config.translations.upgradeButton}
103
- onManage={config.onManageSubscription}
104
- onUpgrade={config.onUpgrade}
105
- />
106
130
  </ScreenLayout>
107
131
  );
108
132
  };
109
-
110
- const styles = StyleSheet.create({
111
- content: {
112
- padding: 16,
113
- gap: 16,
114
- },
115
- });
@@ -1,77 +1,56 @@
1
1
  /**
2
2
  * Subscription Actions Component
3
- * Displays action buttons for subscription management
3
+ * Displays upgrade button for non-premium users
4
4
  */
5
5
 
6
- import React from "react";
6
+ import React, { useMemo } from "react";
7
7
  import { View, StyleSheet, TouchableOpacity } from "react-native";
8
8
  import { useAppDesignTokens, AtomicText } from "@umituz/react-native-design-system";
9
9
 
10
10
  interface SubscriptionActionsProps {
11
11
  isPremium: boolean;
12
- manageButtonLabel?: string;
13
12
  upgradeButtonLabel?: string;
14
- onManage?: () => void;
15
13
  onUpgrade?: () => void;
16
14
  }
17
15
 
18
16
  export const SubscriptionActions: React.FC<SubscriptionActionsProps> = ({
19
17
  isPremium,
20
- manageButtonLabel,
21
18
  upgradeButtonLabel,
22
- onManage,
23
19
  onUpgrade,
24
20
  }) => {
25
21
  const tokens = useAppDesignTokens();
26
22
 
23
+ const styles = useMemo(
24
+ () =>
25
+ StyleSheet.create({
26
+ container: {
27
+ paddingBottom: tokens.spacing.xl,
28
+ },
29
+ primaryButton: {
30
+ paddingVertical: tokens.spacing.md,
31
+ borderRadius: tokens.borderRadius.lg,
32
+ alignItems: "center",
33
+ backgroundColor: tokens.colors.primary,
34
+ },
35
+ buttonText: {
36
+ color: tokens.colors.onPrimary,
37
+ fontWeight: "700",
38
+ },
39
+ }),
40
+ [tokens]
41
+ );
42
+
43
+ if (isPremium || !onUpgrade || !upgradeButtonLabel) {
44
+ return null;
45
+ }
46
+
27
47
  return (
28
48
  <View style={styles.container}>
29
- {isPremium && onManage && manageButtonLabel && (
30
- <TouchableOpacity
31
- style={[
32
- styles.secondaryButton,
33
- { backgroundColor: tokens.colors.surfaceSecondary },
34
- ]}
35
- onPress={onManage}
36
- >
37
- <AtomicText
38
- type="titleMedium"
39
- style={{ color: tokens.colors.textPrimary, fontWeight: "600" }}
40
- >
41
- {manageButtonLabel}
42
- </AtomicText>
43
- </TouchableOpacity>
44
- )}
45
- {!isPremium && onUpgrade && upgradeButtonLabel && (
46
- <TouchableOpacity
47
- style={[styles.primaryButton, { backgroundColor: tokens.colors.primary }]}
48
- onPress={onUpgrade}
49
- >
50
- <AtomicText
51
- type="titleMedium"
52
- style={{ color: tokens.colors.onPrimary, fontWeight: "700" }}
53
- >
54
- {upgradeButtonLabel}
55
- </AtomicText>
56
- </TouchableOpacity>
57
- )}
49
+ <TouchableOpacity style={styles.primaryButton} onPress={onUpgrade}>
50
+ <AtomicText type="titleMedium" style={styles.buttonText}>
51
+ {upgradeButtonLabel}
52
+ </AtomicText>
53
+ </TouchableOpacity>
58
54
  </View>
59
55
  );
60
56
  };
61
-
62
- const styles = StyleSheet.create({
63
- container: {
64
- gap: 12,
65
- paddingBottom: 32,
66
- },
67
- primaryButton: {
68
- paddingVertical: 16,
69
- borderRadius: 12,
70
- alignItems: "center",
71
- },
72
- secondaryButton: {
73
- paddingVertical: 16,
74
- borderRadius: 12,
75
- alignItems: "center",
76
- },
77
- });