@umituz/react-native-subscription 2.12.50 → 2.12.52

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.12.50",
3
+ "version": "2.12.52",
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",
package/src/index.ts CHANGED
@@ -109,6 +109,18 @@ export {
109
109
  type SubscriptionSectionConfig,
110
110
  } from "./presentation/components/sections/SubscriptionSection";
111
111
 
112
+ // =============================================================================
113
+ // PRESENTATION LAYER - Subscription Settings Config Hook
114
+ // =============================================================================
115
+
116
+ export {
117
+ useSubscriptionSettingsConfig,
118
+ type SubscriptionSettingsConfig,
119
+ type SubscriptionSettingsItemConfig,
120
+ type SubscriptionSettingsTranslations,
121
+ type UseSubscriptionSettingsConfigParams,
122
+ } from "./presentation/hooks/useSubscriptionSettingsConfig";
123
+
112
124
  // =============================================================================
113
125
  // PRESENTATION LAYER - Subscription Detail Screen
114
126
  // =============================================================================
@@ -0,0 +1,173 @@
1
+ /**
2
+ * useSubscriptionSettingsConfig Hook
3
+ * Returns ready-to-use config for settings screens
4
+ * Package-driven: all logic handled internally
5
+ */
6
+
7
+ import { useMemo, useCallback } from "react";
8
+ import { Linking } from "react-native";
9
+ import { useCredits } from "./useCredits";
10
+ import { useCustomerInfo } from "../../revenuecat/presentation/hooks/useCustomerInfo";
11
+ import { usePaywallVisibility } from "./usePaywallVisibility";
12
+ import {
13
+ convertPurchasedAt,
14
+ formatDateForLocale,
15
+ calculateDaysRemaining,
16
+ } from "../utils/subscriptionDateUtils";
17
+ import type {
18
+ SubscriptionSettingsConfig,
19
+ SubscriptionStatusType,
20
+ UseSubscriptionSettingsConfigParams,
21
+ } from "../types/SubscriptionSettingsTypes";
22
+
23
+ // Re-export types for convenience
24
+ export type {
25
+ SubscriptionSettingsConfig,
26
+ SubscriptionSettingsItemConfig,
27
+ SubscriptionSettingsTranslations,
28
+ UseSubscriptionSettingsConfigParams,
29
+ } from "../types/SubscriptionSettingsTypes";
30
+
31
+ /**
32
+ * Hook that returns ready-to-use subscription config for settings
33
+ * All business logic handled internally
34
+ */
35
+ export const useSubscriptionSettingsConfig = (
36
+ params: UseSubscriptionSettingsConfigParams
37
+ ): SubscriptionSettingsConfig => {
38
+ const {
39
+ userId,
40
+ isAnonymous = false,
41
+ currentLanguage = "en",
42
+ translations,
43
+ getCreditLimit,
44
+ } = params;
45
+
46
+ // Internal hooks
47
+ const { credits } = useCredits({ userId, enabled: !!userId });
48
+ const { customerInfo } = useCustomerInfo();
49
+ const { openPaywall } = usePaywallVisibility();
50
+
51
+ // Premium status from credits
52
+ const isPremium = credits !== null;
53
+
54
+ // RevenueCat entitlement info
55
+ const premiumEntitlement = customerInfo?.entitlements.active["premium"];
56
+ const expiresAtIso = premiumEntitlement?.expirationDate || null;
57
+ const willRenew = premiumEntitlement?.willRenew || false;
58
+ const purchasedAtIso = convertPurchasedAt(credits?.purchasedAt);
59
+
60
+ // Formatted dates
61
+ const formattedExpirationDate = useMemo(
62
+ () => formatDateForLocale(expiresAtIso, currentLanguage),
63
+ [expiresAtIso, currentLanguage]
64
+ );
65
+
66
+ const formattedPurchaseDate = useMemo(
67
+ () => formatDateForLocale(purchasedAtIso, currentLanguage),
68
+ [purchasedAtIso, currentLanguage]
69
+ );
70
+
71
+ // Days remaining calculation
72
+ const daysRemaining = useMemo(
73
+ () => calculateDaysRemaining(expiresAtIso),
74
+ [expiresAtIso]
75
+ );
76
+
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]);
85
+
86
+ // Status type
87
+ const statusType: SubscriptionStatusType = isPremium ? "active" : "none";
88
+
89
+ // Credits array
90
+ const creditsArray = useMemo(() => {
91
+ if (!credits) return [];
92
+ const total = getCreditLimit
93
+ ? getCreditLimit(credits.imageCredits)
94
+ : credits.imageCredits;
95
+ return [
96
+ {
97
+ id: "image",
98
+ label: translations.imageCreditsLabel || "Image Credits",
99
+ current: credits.imageCredits,
100
+ total,
101
+ },
102
+ ];
103
+ }, [credits, getCreditLimit, translations.imageCreditsLabel]);
104
+
105
+ // Build config
106
+ const config = useMemo(
107
+ (): SubscriptionSettingsConfig => ({
108
+ enabled: true,
109
+ settingsItem: {
110
+ title: translations.title,
111
+ description: translations.description,
112
+ isPremium,
113
+ statusLabel: isPremium
114
+ ? translations.statusActive
115
+ : translations.statusFree,
116
+ icon: "diamond",
117
+ onPress: handleSubscriptionPress,
118
+ },
119
+ sectionConfig: {
120
+ statusType,
121
+ isPremium,
122
+ expirationDate: formattedExpirationDate,
123
+ purchaseDate: formattedPurchaseDate,
124
+ isLifetime: isPremium && !expiresAtIso,
125
+ daysRemaining,
126
+ willRenew,
127
+ credits: creditsArray,
128
+ translations: {
129
+ title: translations.title,
130
+ statusLabel: translations.statusLabel,
131
+ statusActive: translations.statusActive,
132
+ statusExpired: translations.statusExpired,
133
+ statusFree: translations.statusFree,
134
+ statusCanceled: translations.statusCanceled,
135
+ expiresLabel: translations.expiresLabel,
136
+ purchasedLabel: translations.purchasedLabel,
137
+ lifetimeLabel: translations.lifetimeLabel,
138
+ creditsTitle: translations.creditsTitle,
139
+ remainingLabel: translations.remainingLabel,
140
+ manageButton: translations.manageButton,
141
+ upgradeButton: translations.upgradeButton,
142
+ },
143
+ onManageSubscription: handleSubscriptionPress,
144
+ onUpgrade: openPaywall,
145
+ },
146
+ }),
147
+ [
148
+ translations,
149
+ isPremium,
150
+ statusType,
151
+ formattedExpirationDate,
152
+ formattedPurchaseDate,
153
+ expiresAtIso,
154
+ daysRemaining,
155
+ willRenew,
156
+ creditsArray,
157
+ handleSubscriptionPress,
158
+ openPaywall,
159
+ ]
160
+ );
161
+
162
+ if (__DEV__) {
163
+ console.log("[useSubscriptionSettingsConfig]", {
164
+ enabled: config.enabled,
165
+ isPremium,
166
+ isAnonymous,
167
+ hasCredits: !!credits,
168
+ userId: userId || "ANONYMOUS",
169
+ });
170
+ }
171
+
172
+ return config;
173
+ };
@@ -5,8 +5,8 @@
5
5
  */
6
6
 
7
7
  import React from "react";
8
- import { View, ScrollView, StyleSheet } from "react-native";
9
- import { useAppDesignTokens } from "@umituz/react-native-design-system";
8
+ import { StyleSheet } from "react-native";
9
+ import { useAppDesignTokens, ScreenLayout } from "@umituz/react-native-design-system";
10
10
  import { SubscriptionHeader } from "./components/SubscriptionHeader";
11
11
  import { CreditsList } from "./components/CreditsList";
12
12
  import { SubscriptionActions } from "./components/SubscriptionActions";
@@ -39,6 +39,7 @@ export interface SubscriptionDetailConfig {
39
39
  purchaseDate?: string | null;
40
40
  isLifetime?: boolean;
41
41
  daysRemaining?: number | null;
42
+ willRenew?: boolean;
42
43
  credits?: CreditInfo[];
43
44
  translations: SubscriptionDetailTranslations;
44
45
  onManageSubscription?: () => void;
@@ -60,57 +61,55 @@ export const SubscriptionDetailScreen: React.FC<
60
61
  const showCredits = config.credits && config.credits.length > 0;
61
62
 
62
63
  return (
63
- <View style={{ flex: 1 }}>
64
- <ScrollView
65
- style={[styles.container, { backgroundColor: tokens.colors.backgroundPrimary }]}
66
- contentContainerStyle={styles.content}
67
- >
68
- <SubscriptionHeader
69
- statusType={config.statusType}
70
- isPremium={config.isPremium}
71
- isLifetime={config.isLifetime}
72
- expirationDate={config.expirationDate}
73
- purchaseDate={config.purchaseDate}
74
- daysRemaining={config.daysRemaining}
75
- translations={config.translations}
76
- />
77
-
78
- {showCredits && (
79
- <CreditsList
80
- credits={config.credits!}
81
- title={
82
- config.translations.usageTitle || config.translations.creditsTitle
83
- }
84
- description={config.translations.creditsResetInfo}
85
- remainingLabel={config.translations.remainingLabel}
64
+ <ScreenLayout
65
+ scrollable={true}
66
+ edges={['bottom']}
67
+ backgroundColor={tokens.colors.backgroundPrimary}
68
+ contentContainerStyle={styles.content}
69
+ footer={
70
+ config.devTools ? (
71
+ <DevTestSection
72
+ actions={config.devTools.actions}
73
+ title={config.devTools.title}
86
74
  />
87
- )}
75
+ ) : undefined
76
+ }
77
+ >
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
+ />
88
87
 
89
- <SubscriptionActions
90
- isPremium={config.isPremium}
91
- manageButtonLabel={config.translations.manageButton}
92
- upgradeButtonLabel={config.translations.upgradeButton}
93
- onManage={config.onManageSubscription}
94
- onUpgrade={config.onUpgrade}
95
- />
96
- </ScrollView>
97
-
98
- {config.devTools && (
99
- <DevTestSection
100
- actions={config.devTools.actions}
101
- title={config.devTools.title}
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}
102
96
  />
103
97
  )}
104
- </View>
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
+ </ScreenLayout>
105
107
  );
106
108
  };
107
109
 
108
110
  const styles = StyleSheet.create({
109
- container: {
110
- flex: 1,
111
- },
112
- content: {
113
- padding: 16,
114
- gap: 16,
115
- },
111
+ content: {
112
+ padding: 16,
113
+ gap: 16,
114
+ },
116
115
  });
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Subscription Settings Types
3
+ * Type definitions for subscription settings configuration
4
+ */
5
+
6
+ import type { SubscriptionDetailConfig } from "../screens/SubscriptionDetailScreen";
7
+
8
+ /** Status type for subscription state */
9
+ export type SubscriptionStatusType = "none" | "active" | "expired";
10
+
11
+ /** Configuration for settings list item */
12
+ export interface SubscriptionSettingsItemConfig {
13
+ title: string;
14
+ description?: string;
15
+ isPremium: boolean;
16
+ statusLabel: string;
17
+ icon?: string;
18
+ onPress?: () => void;
19
+ }
20
+
21
+ /** Complete subscription settings configuration */
22
+ export interface SubscriptionSettingsConfig {
23
+ /** Whether subscription section should be shown */
24
+ enabled: boolean;
25
+ /** Config for settings list item */
26
+ settingsItem: SubscriptionSettingsItemConfig;
27
+ /** Config for detail screen */
28
+ sectionConfig: SubscriptionDetailConfig;
29
+ }
30
+
31
+ /** Translation strings for subscription settings */
32
+ export interface SubscriptionSettingsTranslations {
33
+ /** Settings item title */
34
+ title: string;
35
+ /** Settings item description */
36
+ description: string;
37
+ /** Status labels */
38
+ statusActive: string;
39
+ statusFree: string;
40
+ statusExpired: string;
41
+ statusCanceled: string;
42
+ /** Detail screen translations */
43
+ statusLabel: string;
44
+ expiresLabel: string;
45
+ purchasedLabel: string;
46
+ lifetimeLabel: string;
47
+ creditsTitle: string;
48
+ remainingLabel: string;
49
+ manageButton: string;
50
+ upgradeButton: string;
51
+ /** Credit label (e.g., "Image Credits") */
52
+ imageCreditsLabel?: string;
53
+ }
54
+
55
+ /** Parameters for useSubscriptionSettingsConfig hook */
56
+ export interface UseSubscriptionSettingsConfigParams {
57
+ /** User ID (required for credits lookup) */
58
+ userId?: string;
59
+ /** Whether user is anonymous */
60
+ isAnonymous?: boolean;
61
+ /** Current language for date formatting */
62
+ currentLanguage?: string;
63
+ /** Translation strings */
64
+ translations: SubscriptionSettingsTranslations;
65
+ /** Credit limit calculator */
66
+ getCreditLimit?: (currentCredits: number) => number;
67
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Subscription Date Utilities
3
+ * Date formatting and calculation utilities for subscription
4
+ */
5
+
6
+ /**
7
+ * Converts Firestore timestamp or Date to ISO string
8
+ */
9
+ export const convertPurchasedAt = (purchasedAt: unknown): string | null => {
10
+ if (!purchasedAt) return null;
11
+
12
+ if (
13
+ typeof purchasedAt === "object" &&
14
+ purchasedAt !== null &&
15
+ "toDate" in purchasedAt
16
+ ) {
17
+ return (purchasedAt as { toDate: () => Date }).toDate().toISOString();
18
+ }
19
+
20
+ if (purchasedAt instanceof Date) {
21
+ return purchasedAt.toISOString();
22
+ }
23
+
24
+ return null;
25
+ };
26
+
27
+ /**
28
+ * Formats a date string for display
29
+ */
30
+ export const formatDateForLocale = (
31
+ dateStr: string | null,
32
+ locale: string
33
+ ): string | null => {
34
+ if (!dateStr) return null;
35
+
36
+ try {
37
+ return new Intl.DateTimeFormat(locale, {
38
+ year: "numeric",
39
+ month: "long",
40
+ day: "numeric",
41
+ }).format(new Date(dateStr));
42
+ } catch {
43
+ return null;
44
+ }
45
+ };
46
+
47
+ /**
48
+ * Calculates days remaining until expiration
49
+ */
50
+ export const calculateDaysRemaining = (
51
+ expiresAtIso: string | null
52
+ ): number | null => {
53
+ if (!expiresAtIso) return null;
54
+
55
+ const end = new Date(expiresAtIso);
56
+ const now = new Date();
57
+ const diff = end.getTime() - now.getTime();
58
+
59
+ return Math.max(0, Math.ceil(diff / (1000 * 60 * 60 * 24)));
60
+ };