@sudobility/building_blocks_rn 0.0.42 → 0.0.44
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/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/src/components/bottom-action-bar/BottomActionBar.d.ts +17 -0
- package/dist/src/components/bottom-action-bar/BottomActionBar.js +33 -0
- package/dist/src/components/bottom-action-bar/index.d.ts +2 -0
- package/dist/src/components/bottom-action-bar/index.js +1 -0
- package/dist/src/components/pages/AppSubscriptionPage.d.ts +35 -0
- package/dist/src/components/pages/AppSubscriptionPage.js +270 -0
- package/dist/src/components/subscription/SubscriptionScreen.d.ts +36 -0
- package/dist/src/components/subscription/SubscriptionScreen.js +201 -0
- package/package.json +4 -2
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Core components (no auth dependency)
|
|
2
2
|
export * from './src/components/header';
|
|
3
3
|
export * from './src/components/footer';
|
|
4
|
+
export * from './src/components/bottom-action-bar';
|
|
4
5
|
export * from './src/components/layout';
|
|
5
6
|
export * from './src/components/settings';
|
|
6
7
|
export * from './src/components/pages';
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Sticky bottom action bar.
|
|
3
|
+
*
|
|
4
|
+
* Pins primary action buttons to the bottom of a screen, just above the
|
|
5
|
+
* bottom tab bar when present, otherwise respecting the home-indicator inset.
|
|
6
|
+
* Place it as the last child of a flex-column screen root, after the scroll
|
|
7
|
+
* or list region, so it never scrolls with content.
|
|
8
|
+
*/
|
|
9
|
+
import React from 'react';
|
|
10
|
+
import type { StyleProp, ViewStyle } from 'react-native';
|
|
11
|
+
export interface BottomActionBarProps {
|
|
12
|
+
/** The button(s) to pin to the bottom. */
|
|
13
|
+
children: React.ReactNode;
|
|
14
|
+
/** Optional container style override. */
|
|
15
|
+
style?: StyleProp<ViewStyle>;
|
|
16
|
+
}
|
|
17
|
+
export declare function BottomActionBar({ children, style }: BottomActionBarProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* @fileoverview Sticky bottom action bar.
|
|
4
|
+
*
|
|
5
|
+
* Pins primary action buttons to the bottom of a screen, just above the
|
|
6
|
+
* bottom tab bar when present, otherwise respecting the home-indicator inset.
|
|
7
|
+
* Place it as the last child of a flex-column screen root, after the scroll
|
|
8
|
+
* or list region, so it never scrolls with content.
|
|
9
|
+
*/
|
|
10
|
+
import { useContext } from 'react';
|
|
11
|
+
import { View, StyleSheet } from 'react-native';
|
|
12
|
+
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
13
|
+
import { BottomTabBarHeightContext } from '@react-navigation/bottom-tabs';
|
|
14
|
+
import { createThemedStyles } from '../../utils/styles';
|
|
15
|
+
export function BottomActionBar({ children, style }) {
|
|
16
|
+
const styles = useStyles();
|
|
17
|
+
const insets = useSafeAreaInsets();
|
|
18
|
+
const tabBarHeight = useContext(BottomTabBarHeightContext);
|
|
19
|
+
// Under a tab bar, the tab bar already covers the home indicator, so the
|
|
20
|
+
// bar sits directly above it with a small constant. Otherwise (pushed/modal
|
|
21
|
+
// screens that cover the tab bar) honor the bottom safe-area inset.
|
|
22
|
+
const paddingBottom = tabBarHeight != null ? 12 : Math.max(insets.bottom, 12);
|
|
23
|
+
return _jsx(View, { style: [styles.bar, { paddingBottom }, style], children: children });
|
|
24
|
+
}
|
|
25
|
+
const useStyles = createThemedStyles(colors => ({
|
|
26
|
+
bar: {
|
|
27
|
+
paddingHorizontal: 16,
|
|
28
|
+
paddingTop: 12,
|
|
29
|
+
borderTopWidth: StyleSheet.hairlineWidth,
|
|
30
|
+
borderTopColor: colors.border,
|
|
31
|
+
backgroundColor: colors.background,
|
|
32
|
+
},
|
|
33
|
+
}));
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { BottomActionBar } from './BottomActionBar';
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { StyleProp, ViewStyle } from 'react-native';
|
|
2
|
+
import type { SubscriptionPageLabels, SubscriptionPageFormatters, AnalyticsTrackingParams } from '../../types';
|
|
3
|
+
import type { SubscriptionPackage } from '../subscription/SubscriptionScreen';
|
|
4
|
+
export interface CurrentSubscriptionStatus {
|
|
5
|
+
isActive: boolean;
|
|
6
|
+
planName?: string;
|
|
7
|
+
expirationDate?: string | null;
|
|
8
|
+
willRenew?: boolean;
|
|
9
|
+
productIdentifier?: string;
|
|
10
|
+
}
|
|
11
|
+
export interface AppSubscriptionPageLabels extends SubscriptionPageLabels {
|
|
12
|
+
statusActive?: string;
|
|
13
|
+
statusInactive?: string;
|
|
14
|
+
statusInactiveMessage?: string;
|
|
15
|
+
labelPlan?: string;
|
|
16
|
+
labelExpires?: string;
|
|
17
|
+
labelRenews?: string;
|
|
18
|
+
yes?: string;
|
|
19
|
+
no?: string;
|
|
20
|
+
}
|
|
21
|
+
export interface AppSubscriptionPageProps {
|
|
22
|
+
currentStatus?: CurrentSubscriptionStatus;
|
|
23
|
+
packages: SubscriptionPackage[];
|
|
24
|
+
labels?: AppSubscriptionPageLabels;
|
|
25
|
+
formatters?: SubscriptionPageFormatters;
|
|
26
|
+
onPurchase: (packageId: string) => Promise<boolean>;
|
|
27
|
+
onRestore: () => Promise<boolean>;
|
|
28
|
+
onPurchaseSuccess?: () => void;
|
|
29
|
+
onRestoreSuccess?: () => void;
|
|
30
|
+
onError?: (title: string, message: string) => void;
|
|
31
|
+
style?: StyleProp<ViewStyle>;
|
|
32
|
+
onTrack?: (params: AnalyticsTrackingParams) => void;
|
|
33
|
+
isLoading?: boolean;
|
|
34
|
+
}
|
|
35
|
+
export declare function AppSubscriptionPage({ currentStatus, packages, labels, formatters, onPurchase, onRestore, onPurchaseSuccess, onRestoreSuccess, onError, style, onTrack, isLoading: externalLoading, }: AppSubscriptionPageProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* @fileoverview Full subscription management page for React Native.
|
|
4
|
+
*
|
|
5
|
+
* Displays the current subscription status (active/inactive badge, plan name,
|
|
6
|
+
* expiration date, auto-renew status), followed by package cards with features,
|
|
7
|
+
* pricing, "Most Popular" badge, and purchase buttons. Also includes a
|
|
8
|
+
* "Restore Purchases" button with loading states.
|
|
9
|
+
*/
|
|
10
|
+
import { useState, useCallback } from 'react';
|
|
11
|
+
import { View, Text, Pressable, ScrollView, ActivityIndicator, } from 'react-native';
|
|
12
|
+
import { createThemedStyles } from '../../utils/styles';
|
|
13
|
+
export function AppSubscriptionPage({ currentStatus, packages, labels = {}, formatters = {}, onPurchase, onRestore, onPurchaseSuccess, onRestoreSuccess, onError, style, onTrack, isLoading: externalLoading, }) {
|
|
14
|
+
const styles = useStyles();
|
|
15
|
+
const [loading, setLoading] = useState(null);
|
|
16
|
+
const formatPrice = formatters.formatPrice ??
|
|
17
|
+
((price, currency) => `${currency} ${price.toFixed(2)}`);
|
|
18
|
+
const handlePurchase = useCallback(async (pkg) => {
|
|
19
|
+
if (loading)
|
|
20
|
+
return;
|
|
21
|
+
setLoading(pkg.id);
|
|
22
|
+
onTrack?.({
|
|
23
|
+
eventType: 'subscription_action',
|
|
24
|
+
componentName: 'AppSubscriptionPage',
|
|
25
|
+
label: 'purchase_tapped',
|
|
26
|
+
params: { package_id: pkg.id },
|
|
27
|
+
});
|
|
28
|
+
try {
|
|
29
|
+
const success = await onPurchase(pkg.id);
|
|
30
|
+
if (success) {
|
|
31
|
+
onPurchaseSuccess?.();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
catch (e) {
|
|
35
|
+
onError?.('Purchase Failed', e instanceof Error ? e.message : 'An error occurred.');
|
|
36
|
+
}
|
|
37
|
+
finally {
|
|
38
|
+
setLoading(null);
|
|
39
|
+
}
|
|
40
|
+
}, [loading, onPurchase, onPurchaseSuccess, onError, onTrack]);
|
|
41
|
+
const handleRestore = useCallback(async () => {
|
|
42
|
+
if (loading)
|
|
43
|
+
return;
|
|
44
|
+
setLoading('restore');
|
|
45
|
+
onTrack?.({
|
|
46
|
+
eventType: 'subscription_action',
|
|
47
|
+
componentName: 'AppSubscriptionPage',
|
|
48
|
+
label: 'restore_tapped',
|
|
49
|
+
});
|
|
50
|
+
try {
|
|
51
|
+
const success = await onRestore();
|
|
52
|
+
if (success) {
|
|
53
|
+
onRestoreSuccess?.();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
catch (e) {
|
|
57
|
+
onError?.('Restore Failed', e instanceof Error ? e.message : 'An error occurred.');
|
|
58
|
+
}
|
|
59
|
+
finally {
|
|
60
|
+
setLoading(null);
|
|
61
|
+
}
|
|
62
|
+
}, [loading, onRestore, onRestoreSuccess, onError, onTrack]);
|
|
63
|
+
return (_jsxs(ScrollView, { style: [styles.container, style], contentContainerStyle: styles.content, children: [currentStatus && (_jsxs(View, { style: styles.statusCard, accessibilityRole: 'summary', accessibilityLabel: `Subscription status: ${currentStatus.isActive ? 'Active' : 'Inactive'}`, children: [_jsx(View, { style: styles.statusHeader, children: _jsx(View, { style: [
|
|
64
|
+
styles.statusBadge,
|
|
65
|
+
currentStatus.isActive
|
|
66
|
+
? styles.statusBadgeActive
|
|
67
|
+
: styles.statusBadgeInactive,
|
|
68
|
+
], children: _jsx(Text, { style: [
|
|
69
|
+
styles.statusBadgeText,
|
|
70
|
+
currentStatus.isActive
|
|
71
|
+
? styles.statusBadgeTextActive
|
|
72
|
+
: styles.statusBadgeTextInactive,
|
|
73
|
+
], children: currentStatus.isActive
|
|
74
|
+
? (labels.statusActive ?? 'Active')
|
|
75
|
+
: (labels.statusInactive ?? 'Inactive') }) }) }), currentStatus.isActive ? (_jsxs(View, { style: styles.statusFields, children: [currentStatus.planName && (_jsxs(View, { style: styles.statusField, children: [_jsx(Text, { style: styles.statusFieldLabel, children: labels.labelPlan ?? 'Plan' }), _jsx(Text, { style: styles.statusFieldValue, children: currentStatus.planName })] })), currentStatus.expirationDate && (_jsxs(View, { style: styles.statusField, children: [_jsx(Text, { style: styles.statusFieldLabel, children: labels.labelExpires ?? 'Expires' }), _jsx(Text, { style: styles.statusFieldValue, children: currentStatus.expirationDate })] })), currentStatus.willRenew !== undefined && (_jsxs(View, { style: styles.statusField, children: [_jsx(Text, { style: styles.statusFieldLabel, children: labels.labelRenews ?? 'Auto-renews' }), _jsx(Text, { style: styles.statusFieldValue, children: currentStatus.willRenew
|
|
76
|
+
? (labels.yes ?? 'Yes')
|
|
77
|
+
: (labels.no ?? 'No') })] }))] })) : (_jsx(Text, { style: styles.statusInactiveMessage, children: labels.statusInactiveMessage ??
|
|
78
|
+
'Subscribe to unlock premium features' }))] })), externalLoading && (_jsx(View, { style: styles.loadingContainer, children: _jsx(ActivityIndicator, { size: 'large', color: styles.loadingIndicator.color, accessibilityLabel: 'Loading subscription packages' }) })), !externalLoading && packages.length > 0 && (_jsx(View, { style: styles.packageList, accessibilityRole: 'list', children: packages.map(pkg => (_jsxs(View, { style: [
|
|
79
|
+
styles.packageCard,
|
|
80
|
+
pkg.isMostPopular && styles.packageCardPopular,
|
|
81
|
+
], accessibilityRole: 'summary', accessibilityLabel: `${pkg.title}, ${formatPrice(pkg.price, pkg.currency)}${pkg.isMostPopular ? ', Most Popular' : ''}${pkg.isCurrent ? ', Current Plan' : ''}`, children: [pkg.isMostPopular && (_jsx(View, { style: styles.popularBadge, children: _jsx(Text, { style: styles.popularBadgeText, children: labels.mostPopular ?? 'Most Popular' }) })), _jsx(Text, { style: styles.packageTitle, children: pkg.title }), pkg.description && (_jsx(Text, { style: styles.packageDescription, children: pkg.description })), _jsx(Text, { style: styles.packagePrice, children: formatPrice(pkg.price, pkg.currency) }), pkg.features && pkg.features.length > 0 && (_jsx(View, { style: styles.featureList, children: pkg.features.map((feature, i) => (_jsxs(Text, { style: styles.featureItem, children: ['\u2713', " ", feature] }, i))) })), pkg.isCurrent ? (_jsx(View, { style: styles.currentBadge, children: _jsx(Text, { style: styles.currentBadgeText, children: labels.currentlyActive ?? 'Current Plan' }) })) : (_jsx(Pressable, { style: [
|
|
82
|
+
styles.purchaseButton,
|
|
83
|
+
loading === pkg.id && styles.purchaseButtonDisabled,
|
|
84
|
+
], onPress: () => handlePurchase(pkg), disabled: loading !== null, accessibilityRole: 'button', accessibilityLabel: `${labels.purchase ?? 'Subscribe'} to ${pkg.title}`, accessibilityState: {
|
|
85
|
+
disabled: loading !== null,
|
|
86
|
+
busy: loading === pkg.id,
|
|
87
|
+
}, children: loading === pkg.id ? (_jsx(ActivityIndicator, { color: '#ffffff', size: 'small' })) : (_jsx(Text, { style: styles.purchaseButtonText, children: labels.purchase ?? 'Subscribe' })) }))] }, pkg.id))) })), _jsx(Pressable, { style: [
|
|
88
|
+
styles.restoreButton,
|
|
89
|
+
loading === 'restore' && styles.restoreButtonDisabled,
|
|
90
|
+
], onPress: handleRestore, disabled: loading !== null, accessibilityRole: 'button', accessibilityLabel: labels.restore ?? 'Restore Purchases', accessibilityState: {
|
|
91
|
+
disabled: loading !== null,
|
|
92
|
+
busy: loading === 'restore',
|
|
93
|
+
}, children: loading === 'restore' ? (_jsx(ActivityIndicator, { color: styles.restoreText.color, size: 'small' })) : (_jsx(Text, { style: styles.restoreText, children: labels.restore ?? 'Restore Purchases' })) }), labels.restoreDescription && (_jsx(Text, { style: styles.restoreDescription, children: labels.restoreDescription }))] }));
|
|
94
|
+
}
|
|
95
|
+
const useStyles = createThemedStyles(colors => ({
|
|
96
|
+
container: {
|
|
97
|
+
flex: 1,
|
|
98
|
+
backgroundColor: colors.background,
|
|
99
|
+
},
|
|
100
|
+
content: {
|
|
101
|
+
padding: 16,
|
|
102
|
+
paddingBottom: 32,
|
|
103
|
+
},
|
|
104
|
+
// Status card
|
|
105
|
+
statusCard: {
|
|
106
|
+
backgroundColor: colors.card,
|
|
107
|
+
borderRadius: 16,
|
|
108
|
+
padding: 20,
|
|
109
|
+
borderWidth: 1,
|
|
110
|
+
borderColor: colors.border,
|
|
111
|
+
marginBottom: 16,
|
|
112
|
+
},
|
|
113
|
+
statusHeader: {
|
|
114
|
+
flexDirection: 'row',
|
|
115
|
+
alignItems: 'center',
|
|
116
|
+
marginBottom: 12,
|
|
117
|
+
},
|
|
118
|
+
statusBadge: {
|
|
119
|
+
borderRadius: 12,
|
|
120
|
+
paddingHorizontal: 10,
|
|
121
|
+
paddingVertical: 4,
|
|
122
|
+
},
|
|
123
|
+
statusBadgeActive: {
|
|
124
|
+
backgroundColor: colors.successBg,
|
|
125
|
+
},
|
|
126
|
+
statusBadgeInactive: {
|
|
127
|
+
backgroundColor: colors.surfaceSecondary,
|
|
128
|
+
},
|
|
129
|
+
statusBadgeText: {
|
|
130
|
+
fontSize: 13,
|
|
131
|
+
fontWeight: '600',
|
|
132
|
+
},
|
|
133
|
+
statusBadgeTextActive: {
|
|
134
|
+
color: colors.successText,
|
|
135
|
+
},
|
|
136
|
+
statusBadgeTextInactive: {
|
|
137
|
+
color: colors.textSecondary,
|
|
138
|
+
},
|
|
139
|
+
statusFields: {
|
|
140
|
+
gap: 8,
|
|
141
|
+
},
|
|
142
|
+
statusField: {
|
|
143
|
+
flexDirection: 'row',
|
|
144
|
+
justifyContent: 'space-between',
|
|
145
|
+
alignItems: 'center',
|
|
146
|
+
},
|
|
147
|
+
statusFieldLabel: {
|
|
148
|
+
fontSize: 14,
|
|
149
|
+
color: colors.textSecondary,
|
|
150
|
+
},
|
|
151
|
+
statusFieldValue: {
|
|
152
|
+
fontSize: 14,
|
|
153
|
+
fontWeight: '500',
|
|
154
|
+
color: colors.text,
|
|
155
|
+
},
|
|
156
|
+
statusInactiveMessage: {
|
|
157
|
+
fontSize: 14,
|
|
158
|
+
color: colors.textSecondary,
|
|
159
|
+
},
|
|
160
|
+
// Loading
|
|
161
|
+
loadingContainer: {
|
|
162
|
+
paddingVertical: 48,
|
|
163
|
+
alignItems: 'center',
|
|
164
|
+
},
|
|
165
|
+
loadingIndicator: {
|
|
166
|
+
color: colors.primary,
|
|
167
|
+
},
|
|
168
|
+
// Package cards (mirrors SubscriptionScreen styles)
|
|
169
|
+
packageList: {
|
|
170
|
+
gap: 16,
|
|
171
|
+
},
|
|
172
|
+
packageCard: {
|
|
173
|
+
backgroundColor: colors.card,
|
|
174
|
+
borderRadius: 16,
|
|
175
|
+
padding: 20,
|
|
176
|
+
borderWidth: 1,
|
|
177
|
+
borderColor: colors.border,
|
|
178
|
+
},
|
|
179
|
+
packageCardPopular: {
|
|
180
|
+
borderColor: colors.primary,
|
|
181
|
+
borderWidth: 2,
|
|
182
|
+
},
|
|
183
|
+
popularBadge: {
|
|
184
|
+
backgroundColor: colors.primary,
|
|
185
|
+
borderRadius: 12,
|
|
186
|
+
paddingHorizontal: 10,
|
|
187
|
+
paddingVertical: 4,
|
|
188
|
+
alignSelf: 'flex-start',
|
|
189
|
+
marginBottom: 8,
|
|
190
|
+
},
|
|
191
|
+
popularBadgeText: {
|
|
192
|
+
color: '#ffffff',
|
|
193
|
+
fontSize: 12,
|
|
194
|
+
fontWeight: '600',
|
|
195
|
+
},
|
|
196
|
+
packageTitle: {
|
|
197
|
+
fontSize: 20,
|
|
198
|
+
fontWeight: '600',
|
|
199
|
+
color: colors.text,
|
|
200
|
+
},
|
|
201
|
+
packageDescription: {
|
|
202
|
+
fontSize: 14,
|
|
203
|
+
color: colors.textSecondary,
|
|
204
|
+
marginTop: 4,
|
|
205
|
+
},
|
|
206
|
+
packagePrice: {
|
|
207
|
+
fontSize: 28,
|
|
208
|
+
fontWeight: '700',
|
|
209
|
+
color: colors.text,
|
|
210
|
+
marginTop: 8,
|
|
211
|
+
},
|
|
212
|
+
featureList: {
|
|
213
|
+
marginTop: 12,
|
|
214
|
+
gap: 6,
|
|
215
|
+
},
|
|
216
|
+
featureItem: {
|
|
217
|
+
fontSize: 14,
|
|
218
|
+
color: colors.textSecondary,
|
|
219
|
+
},
|
|
220
|
+
purchaseButton: {
|
|
221
|
+
backgroundColor: colors.primary,
|
|
222
|
+
borderRadius: 10,
|
|
223
|
+
paddingVertical: 14,
|
|
224
|
+
alignItems: 'center',
|
|
225
|
+
marginTop: 16,
|
|
226
|
+
minHeight: 48,
|
|
227
|
+
justifyContent: 'center',
|
|
228
|
+
},
|
|
229
|
+
purchaseButtonDisabled: {
|
|
230
|
+
opacity: 0.6,
|
|
231
|
+
},
|
|
232
|
+
purchaseButtonText: {
|
|
233
|
+
color: '#ffffff',
|
|
234
|
+
fontSize: 16,
|
|
235
|
+
fontWeight: '600',
|
|
236
|
+
},
|
|
237
|
+
currentBadge: {
|
|
238
|
+
borderRadius: 10,
|
|
239
|
+
paddingVertical: 14,
|
|
240
|
+
alignItems: 'center',
|
|
241
|
+
marginTop: 16,
|
|
242
|
+
backgroundColor: colors.surfaceSecondary,
|
|
243
|
+
},
|
|
244
|
+
currentBadgeText: {
|
|
245
|
+
color: colors.textSecondary,
|
|
246
|
+
fontSize: 16,
|
|
247
|
+
fontWeight: '500',
|
|
248
|
+
},
|
|
249
|
+
restoreButton: {
|
|
250
|
+
marginTop: 24,
|
|
251
|
+
paddingVertical: 12,
|
|
252
|
+
alignItems: 'center',
|
|
253
|
+
minHeight: 44,
|
|
254
|
+
justifyContent: 'center',
|
|
255
|
+
},
|
|
256
|
+
restoreButtonDisabled: {
|
|
257
|
+
opacity: 0.6,
|
|
258
|
+
},
|
|
259
|
+
restoreText: {
|
|
260
|
+
color: colors.primary,
|
|
261
|
+
fontSize: 15,
|
|
262
|
+
fontWeight: '500',
|
|
263
|
+
},
|
|
264
|
+
restoreDescription: {
|
|
265
|
+
fontSize: 13,
|
|
266
|
+
color: colors.textMuted,
|
|
267
|
+
textAlign: 'center',
|
|
268
|
+
marginTop: 4,
|
|
269
|
+
},
|
|
270
|
+
}));
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { StyleProp, ViewStyle } from 'react-native';
|
|
2
|
+
import type { SubscriptionPageLabels, SubscriptionPageFormatters, AnalyticsTrackingParams } from '../../types';
|
|
3
|
+
export interface SubscriptionPackage {
|
|
4
|
+
id: string;
|
|
5
|
+
title: string;
|
|
6
|
+
description?: string;
|
|
7
|
+
price: number;
|
|
8
|
+
currency: string;
|
|
9
|
+
period: 'monthly' | 'yearly' | 'lifetime';
|
|
10
|
+
features?: string[];
|
|
11
|
+
isMostPopular?: boolean;
|
|
12
|
+
isCurrent?: boolean;
|
|
13
|
+
}
|
|
14
|
+
export interface SubscriptionScreenProps {
|
|
15
|
+
/** Available subscription packages */
|
|
16
|
+
packages: SubscriptionPackage[];
|
|
17
|
+
/** Labels for UI text */
|
|
18
|
+
labels?: SubscriptionPageLabels;
|
|
19
|
+
/** Formatters for prices and periods */
|
|
20
|
+
formatters?: SubscriptionPageFormatters;
|
|
21
|
+
/** Called when a package is purchased */
|
|
22
|
+
onPurchase: (packageId: string) => Promise<boolean>;
|
|
23
|
+
/** Called when restore purchases is pressed */
|
|
24
|
+
onRestore: () => Promise<boolean>;
|
|
25
|
+
/** Called on successful purchase */
|
|
26
|
+
onPurchaseSuccess?: () => void;
|
|
27
|
+
/** Called on successful restore */
|
|
28
|
+
onRestoreSuccess?: () => void;
|
|
29
|
+
/** Called on error */
|
|
30
|
+
onError?: (title: string, message: string) => void;
|
|
31
|
+
/** Custom style */
|
|
32
|
+
style?: StyleProp<ViewStyle>;
|
|
33
|
+
/** Analytics tracking */
|
|
34
|
+
onTrack?: (params: AnalyticsTrackingParams) => void;
|
|
35
|
+
}
|
|
36
|
+
export declare function SubscriptionScreen({ packages, labels, formatters, onPurchase, onRestore, onPurchaseSuccess, onRestoreSuccess, onError, style, onTrack, }: SubscriptionScreenProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* @fileoverview Subscription plan selector screen for React Native.
|
|
4
|
+
*
|
|
5
|
+
* Simpler plan selector (no current status display) with package cards,
|
|
6
|
+
* purchase buttons, and a restore purchases flow. Uses the same
|
|
7
|
+
* purchase/restore callback pattern as AppSubscriptionPage.
|
|
8
|
+
*/
|
|
9
|
+
import { useState, useCallback } from 'react';
|
|
10
|
+
import { View, Text, Pressable, ScrollView, ActivityIndicator, } from 'react-native';
|
|
11
|
+
import { createThemedStyles } from '../../utils/styles';
|
|
12
|
+
export function SubscriptionScreen({ packages, labels = {}, formatters = {}, onPurchase, onRestore, onPurchaseSuccess, onRestoreSuccess, onError, style, onTrack, }) {
|
|
13
|
+
const styles = useStyles();
|
|
14
|
+
const [loading, setLoading] = useState(null);
|
|
15
|
+
const formatPrice = formatters.formatPrice ??
|
|
16
|
+
((price, currency) => `${currency} ${price.toFixed(2)}`);
|
|
17
|
+
const handlePurchase = useCallback(async (pkg) => {
|
|
18
|
+
if (loading)
|
|
19
|
+
return;
|
|
20
|
+
setLoading(pkg.id);
|
|
21
|
+
onTrack?.({
|
|
22
|
+
eventType: 'subscription_action',
|
|
23
|
+
componentName: 'SubscriptionScreen',
|
|
24
|
+
label: 'purchase_tapped',
|
|
25
|
+
params: { package_id: pkg.id },
|
|
26
|
+
});
|
|
27
|
+
try {
|
|
28
|
+
const success = await onPurchase(pkg.id);
|
|
29
|
+
if (success) {
|
|
30
|
+
onPurchaseSuccess?.();
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
catch (e) {
|
|
34
|
+
onError?.('Purchase Failed', e instanceof Error ? e.message : 'An error occurred.');
|
|
35
|
+
}
|
|
36
|
+
finally {
|
|
37
|
+
setLoading(null);
|
|
38
|
+
}
|
|
39
|
+
}, [loading, onPurchase, onPurchaseSuccess, onError, onTrack]);
|
|
40
|
+
const handleRestore = useCallback(async () => {
|
|
41
|
+
if (loading)
|
|
42
|
+
return;
|
|
43
|
+
setLoading('restore');
|
|
44
|
+
onTrack?.({
|
|
45
|
+
eventType: 'subscription_action',
|
|
46
|
+
componentName: 'SubscriptionScreen',
|
|
47
|
+
label: 'restore_tapped',
|
|
48
|
+
});
|
|
49
|
+
try {
|
|
50
|
+
const success = await onRestore();
|
|
51
|
+
if (success) {
|
|
52
|
+
onRestoreSuccess?.();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
catch (e) {
|
|
56
|
+
onError?.('Restore Failed', e instanceof Error ? e.message : 'An error occurred.');
|
|
57
|
+
}
|
|
58
|
+
finally {
|
|
59
|
+
setLoading(null);
|
|
60
|
+
}
|
|
61
|
+
}, [loading, onRestore, onRestoreSuccess, onError, onTrack]);
|
|
62
|
+
return (_jsxs(ScrollView, { style: [styles.container, style], contentContainerStyle: styles.content, children: [_jsx(Text, { style: styles.title, accessibilityRole: 'header', children: labels.title ?? 'Subscription' }), labels.subtitle && (_jsx(Text, { style: styles.subtitle, children: labels.subtitle })), _jsx(View, { style: styles.packageList, accessibilityRole: 'list', children: packages.map(pkg => (_jsxs(View, { style: [
|
|
63
|
+
styles.packageCard,
|
|
64
|
+
pkg.isMostPopular && styles.packageCardPopular,
|
|
65
|
+
], accessibilityRole: 'summary', accessibilityLabel: `${pkg.title}, ${formatPrice(pkg.price, pkg.currency)}${pkg.isMostPopular ? ', Most Popular' : ''}${pkg.isCurrent ? ', Current Plan' : ''}`, children: [pkg.isMostPopular && (_jsx(View, { style: styles.popularBadge, children: _jsx(Text, { style: styles.popularBadgeText, children: labels.mostPopular ?? 'Most Popular' }) })), _jsx(Text, { style: styles.packageTitle, children: pkg.title }), pkg.description && (_jsx(Text, { style: styles.packageDescription, children: pkg.description })), _jsx(Text, { style: styles.packagePrice, children: formatPrice(pkg.price, pkg.currency) }), pkg.features && pkg.features.length > 0 && (_jsx(View, { style: styles.featureList, children: pkg.features.map((feature, i) => (_jsxs(Text, { style: styles.featureItem, children: ['\u2713', " ", feature] }, i))) })), pkg.isCurrent ? (_jsx(View, { style: styles.currentBadge, children: _jsx(Text, { style: styles.currentBadgeText, children: labels.currentlyActive ?? 'Current Plan' }) })) : (_jsx(Pressable, { style: [
|
|
66
|
+
styles.purchaseButton,
|
|
67
|
+
loading === pkg.id && styles.purchaseButtonDisabled,
|
|
68
|
+
], onPress: () => handlePurchase(pkg), disabled: loading !== null, accessibilityRole: 'button', accessibilityLabel: `${labels.purchase ?? 'Subscribe'} to ${pkg.title}`, accessibilityState: {
|
|
69
|
+
disabled: loading !== null,
|
|
70
|
+
busy: loading === pkg.id,
|
|
71
|
+
}, children: loading === pkg.id ? (_jsx(ActivityIndicator, { color: '#ffffff', size: 'small' })) : (_jsx(Text, { style: styles.purchaseButtonText, children: labels.purchase ?? 'Subscribe' })) }))] }, pkg.id))) }), _jsx(Pressable, { style: [
|
|
72
|
+
styles.restoreButton,
|
|
73
|
+
loading === 'restore' && styles.restoreButtonDisabled,
|
|
74
|
+
], onPress: handleRestore, disabled: loading !== null, accessibilityRole: 'button', accessibilityLabel: labels.restore ?? 'Restore Purchases', accessibilityState: {
|
|
75
|
+
disabled: loading !== null,
|
|
76
|
+
busy: loading === 'restore',
|
|
77
|
+
}, children: loading === 'restore' ? (_jsx(ActivityIndicator, { color: styles.restoreText.color, size: 'small' })) : (_jsx(Text, { style: styles.restoreText, children: labels.restore ?? 'Restore Purchases' })) }), labels.restoreDescription && (_jsx(Text, { style: styles.restoreDescription, children: labels.restoreDescription }))] }));
|
|
78
|
+
}
|
|
79
|
+
const useStyles = createThemedStyles(colors => ({
|
|
80
|
+
container: {
|
|
81
|
+
flex: 1,
|
|
82
|
+
backgroundColor: colors.background,
|
|
83
|
+
},
|
|
84
|
+
content: {
|
|
85
|
+
padding: 16,
|
|
86
|
+
paddingBottom: 32,
|
|
87
|
+
},
|
|
88
|
+
title: {
|
|
89
|
+
fontSize: 28,
|
|
90
|
+
fontWeight: '700',
|
|
91
|
+
color: colors.text,
|
|
92
|
+
marginBottom: 4,
|
|
93
|
+
},
|
|
94
|
+
subtitle: {
|
|
95
|
+
fontSize: 15,
|
|
96
|
+
color: colors.textSecondary,
|
|
97
|
+
marginBottom: 20,
|
|
98
|
+
},
|
|
99
|
+
packageList: {
|
|
100
|
+
gap: 16,
|
|
101
|
+
marginTop: 16,
|
|
102
|
+
},
|
|
103
|
+
packageCard: {
|
|
104
|
+
backgroundColor: colors.card,
|
|
105
|
+
borderRadius: 16,
|
|
106
|
+
padding: 20,
|
|
107
|
+
borderWidth: 1,
|
|
108
|
+
borderColor: colors.border,
|
|
109
|
+
},
|
|
110
|
+
packageCardPopular: {
|
|
111
|
+
borderColor: colors.primary,
|
|
112
|
+
borderWidth: 2,
|
|
113
|
+
},
|
|
114
|
+
popularBadge: {
|
|
115
|
+
backgroundColor: colors.primary,
|
|
116
|
+
borderRadius: 12,
|
|
117
|
+
paddingHorizontal: 10,
|
|
118
|
+
paddingVertical: 4,
|
|
119
|
+
alignSelf: 'flex-start',
|
|
120
|
+
marginBottom: 8,
|
|
121
|
+
},
|
|
122
|
+
popularBadgeText: {
|
|
123
|
+
color: '#ffffff',
|
|
124
|
+
fontSize: 12,
|
|
125
|
+
fontWeight: '600',
|
|
126
|
+
},
|
|
127
|
+
packageTitle: {
|
|
128
|
+
fontSize: 20,
|
|
129
|
+
fontWeight: '600',
|
|
130
|
+
color: colors.text,
|
|
131
|
+
},
|
|
132
|
+
packageDescription: {
|
|
133
|
+
fontSize: 14,
|
|
134
|
+
color: colors.textSecondary,
|
|
135
|
+
marginTop: 4,
|
|
136
|
+
},
|
|
137
|
+
packagePrice: {
|
|
138
|
+
fontSize: 28,
|
|
139
|
+
fontWeight: '700',
|
|
140
|
+
color: colors.text,
|
|
141
|
+
marginTop: 8,
|
|
142
|
+
},
|
|
143
|
+
featureList: {
|
|
144
|
+
marginTop: 12,
|
|
145
|
+
gap: 6,
|
|
146
|
+
},
|
|
147
|
+
featureItem: {
|
|
148
|
+
fontSize: 14,
|
|
149
|
+
color: colors.textSecondary,
|
|
150
|
+
},
|
|
151
|
+
purchaseButton: {
|
|
152
|
+
backgroundColor: colors.primary,
|
|
153
|
+
borderRadius: 10,
|
|
154
|
+
paddingVertical: 14,
|
|
155
|
+
alignItems: 'center',
|
|
156
|
+
marginTop: 16,
|
|
157
|
+
minHeight: 48,
|
|
158
|
+
justifyContent: 'center',
|
|
159
|
+
},
|
|
160
|
+
purchaseButtonDisabled: {
|
|
161
|
+
opacity: 0.6,
|
|
162
|
+
},
|
|
163
|
+
purchaseButtonText: {
|
|
164
|
+
color: '#ffffff',
|
|
165
|
+
fontSize: 16,
|
|
166
|
+
fontWeight: '600',
|
|
167
|
+
},
|
|
168
|
+
currentBadge: {
|
|
169
|
+
borderRadius: 10,
|
|
170
|
+
paddingVertical: 14,
|
|
171
|
+
alignItems: 'center',
|
|
172
|
+
marginTop: 16,
|
|
173
|
+
backgroundColor: colors.surfaceSecondary,
|
|
174
|
+
},
|
|
175
|
+
currentBadgeText: {
|
|
176
|
+
color: colors.textSecondary,
|
|
177
|
+
fontSize: 16,
|
|
178
|
+
fontWeight: '500',
|
|
179
|
+
},
|
|
180
|
+
restoreButton: {
|
|
181
|
+
marginTop: 24,
|
|
182
|
+
paddingVertical: 12,
|
|
183
|
+
alignItems: 'center',
|
|
184
|
+
minHeight: 44,
|
|
185
|
+
justifyContent: 'center',
|
|
186
|
+
},
|
|
187
|
+
restoreButtonDisabled: {
|
|
188
|
+
opacity: 0.6,
|
|
189
|
+
},
|
|
190
|
+
restoreText: {
|
|
191
|
+
color: colors.primary,
|
|
192
|
+
fontSize: 15,
|
|
193
|
+
fontWeight: '500',
|
|
194
|
+
},
|
|
195
|
+
restoreDescription: {
|
|
196
|
+
fontSize: 13,
|
|
197
|
+
color: colors.textMuted,
|
|
198
|
+
textAlign: 'center',
|
|
199
|
+
marginTop: 4,
|
|
200
|
+
},
|
|
201
|
+
}));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sudobility/building_blocks_rn",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.44",
|
|
4
4
|
"description": "Higher-level shared UI building blocks for Sudobility React Native apps",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -46,10 +46,11 @@
|
|
|
46
46
|
"react-native-safe-area-context": ">=4.0.0",
|
|
47
47
|
"@react-navigation/native": ">=7.0.0",
|
|
48
48
|
"@react-navigation/native-stack": ">=7.0.0",
|
|
49
|
+
"@react-navigation/bottom-tabs": ">=7.0.0",
|
|
49
50
|
"i18next": "^23.0.0 || ^24.0.0 || ^25.0.0",
|
|
50
51
|
"react-i18next": "^14.0.0 || ^15.0.0 || ^16.0.0",
|
|
51
52
|
"@react-native-async-storage/async-storage": ">=1.0.0",
|
|
52
|
-
"@sudobility/design": "^1.1.
|
|
53
|
+
"@sudobility/design": "^1.1.32",
|
|
53
54
|
"react-native-in-app-review": ">=4.4.2"
|
|
54
55
|
},
|
|
55
56
|
"peerDependenciesMeta": {
|
|
@@ -80,6 +81,7 @@
|
|
|
80
81
|
"@react-native-async-storage/async-storage": "2.2.0",
|
|
81
82
|
"@react-navigation/native": "^7.1.28",
|
|
82
83
|
"@react-navigation/native-stack": "^7.10.1",
|
|
84
|
+
"@react-navigation/bottom-tabs": "^7.4.0",
|
|
83
85
|
"@sudobility/types": "^1.9.62",
|
|
84
86
|
"@tanstack/react-query": "^5.90.19",
|
|
85
87
|
"@types/react": "~19.1.0",
|