@umituz/react-native-subscription 2.12.47 → 2.12.51
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/index.ts +12 -0
- package/src/presentation/hooks/useSubscriptionSettingsConfig.ts +173 -0
- package/src/presentation/screens/SubscriptionDetailScreen.tsx +46 -47
- package/src/presentation/types/SubscriptionSettingsTypes.ts +67 -0
- package/src/presentation/utils/subscriptionDateUtils.ts +60 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-subscription",
|
|
3
|
-
"version": "2.12.
|
|
3
|
+
"version": "2.12.51",
|
|
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 {
|
|
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
|
-
<
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
+
};
|