@umituz/react-native-subscription 2.44.0 → 2.45.0

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.
@@ -0,0 +1,201 @@
1
+ /**
2
+ * Paywall Feedback Screen Parts
3
+ * Sub-components extracted for better maintainability
4
+ */
5
+
6
+ import React from "react";
7
+ import { View, TouchableOpacity, ScrollView } from "react-native";
8
+ import { AtomicText, AtomicIcon } from "@umituz/react-native-design-system/atoms";
9
+ import { useSafeAreaInsets } from "@umituz/react-native-design-system/safe-area";
10
+ import { FeedbackOption } from "./FeedbackOption";
11
+ import type { PaywallFeedbackTranslations } from "./PaywallFeedbackScreen.types";
12
+
13
+ // ============================================================================
14
+ // CLOSE BUTTON
15
+ // ============================================================================
16
+
17
+ interface FeedbackCloseButtonProps {
18
+ onPress: () => void;
19
+ topInset: number;
20
+ backgroundColor: string;
21
+ iconColor: string;
22
+ }
23
+
24
+ export const FeedbackCloseButton: React.FC<FeedbackCloseButtonProps> = ({
25
+ onPress,
26
+ topInset,
27
+ backgroundColor,
28
+ iconColor,
29
+ }) => (
30
+ <TouchableOpacity
31
+ onPress={onPress}
32
+ style={[{
33
+ position: 'absolute',
34
+ top: Math.max(topInset, 12),
35
+ right: 12,
36
+ width: 40,
37
+ height: 40,
38
+ borderRadius: 20,
39
+ zIndex: 1000,
40
+ justifyContent: 'center',
41
+ alignItems: 'center',
42
+ }, { backgroundColor }]}
43
+ hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
44
+ >
45
+ <AtomicIcon name="close-outline" size="md" customColor={iconColor} />
46
+ </TouchableOpacity>
47
+ );
48
+
49
+ // ============================================================================
50
+ // HEADER
51
+ // ============================================================================
52
+
53
+ interface FeedbackHeaderProps {
54
+ title: string;
55
+ subtitle?: string;
56
+ titleColor: string;
57
+ subtitleColor: string;
58
+ style: any;
59
+ }
60
+
61
+ export const FeedbackHeader: React.FC<FeedbackHeaderProps> = ({
62
+ title,
63
+ subtitle,
64
+ titleColor,
65
+ subtitleColor,
66
+ style,
67
+ }) => (
68
+ <View style={style}>
69
+ <AtomicText
70
+ type="headlineMedium"
71
+ style={[{
72
+ marginBottom: 12,
73
+ }, { color: titleColor }]}
74
+ >
75
+ {title}
76
+ </AtomicText>
77
+ {subtitle && (
78
+ <AtomicText
79
+ type="bodyMedium"
80
+ style={[{
81
+ lineHeight: 24,
82
+ }, { color: subtitleColor }]}
83
+ >
84
+ {subtitle}
85
+ </AtomicText>
86
+ )}
87
+ </View>
88
+ );
89
+
90
+ // ============================================================================
91
+ // OPTIONS LIST
92
+ // ============================================================================
93
+
94
+ const FEEDBACK_OPTION_IDS = [
95
+ "too_expensive",
96
+ "no_need",
97
+ "trying_out",
98
+ "technical_issues",
99
+ "other",
100
+ ] as const;
101
+
102
+ interface FeedbackOptionsListProps {
103
+ translations: PaywallFeedbackTranslations;
104
+ selectedReason: string | null;
105
+ otherText: string;
106
+ onSelectReason: (reason: string) => void;
107
+ onSetOtherText: (text: string) => void;
108
+ }
109
+
110
+ export const FeedbackOptionsList: React.FC<FeedbackOptionsListProps> = ({
111
+ translations,
112
+ selectedReason,
113
+ otherText,
114
+ onSelectReason,
115
+ onSetOtherText,
116
+ }) => (
117
+ <View style={{ gap: 12 }}>
118
+ {FEEDBACK_OPTION_IDS.map((optionId) => {
119
+ const isSelected = selectedReason === optionId;
120
+ const isOther = optionId === "other";
121
+ const showInput = isSelected && isOther;
122
+
123
+ return (
124
+ <FeedbackOption
125
+ key={optionId}
126
+ isSelected={isSelected}
127
+ text={translations.reasons[optionId]}
128
+ showInput={showInput}
129
+ placeholder={translations.otherPlaceholder}
130
+ inputValue={otherText}
131
+ onSelect={() => onSelectReason(optionId)}
132
+ onChangeText={onSetOtherText}
133
+ />
134
+ );
135
+ })}
136
+ </View>
137
+ );
138
+
139
+ // ============================================================================
140
+ // SUBMIT BUTTON
141
+ // ============================================================================
142
+
143
+ interface FeedbackSubmitButtonProps {
144
+ title: string;
145
+ canSubmit: boolean;
146
+ backgroundColor: string;
147
+ textColor: string;
148
+ onPress: () => void;
149
+ bottomInset: number;
150
+ }
151
+
152
+ export const FeedbackSubmitButton: React.FC<FeedbackSubmitButtonProps> = ({
153
+ title,
154
+ canSubmit,
155
+ backgroundColor,
156
+ textColor,
157
+ onPress,
158
+ bottomInset,
159
+ }) => (
160
+ <View style={[{
161
+ position: 'absolute',
162
+ bottom: 0,
163
+ left: 0,
164
+ right: 0,
165
+ paddingHorizontal: 24,
166
+ paddingTop: 18,
167
+ paddingBottom: Math.max(bottomInset, 18),
168
+ borderTopWidth: 1,
169
+ borderTopColor: 'rgba(0,0,0,0.1)',
170
+ }, { backgroundColor: 'rgba(255,255,255,0.98)' }]}>
171
+ <TouchableOpacity
172
+ style={[{
173
+ borderRadius: 16,
174
+ paddingVertical: 18,
175
+ alignItems: 'center',
176
+ shadowColor: "#000",
177
+ shadowOffset: { width: 0, height: 2 },
178
+ shadowOpacity: 0.1,
179
+ shadowRadius: 4,
180
+ elevation: 3,
181
+ }, {
182
+ backgroundColor: canSubmit ? backgroundColor : 'rgba(0,0,0,0.1)',
183
+ opacity: canSubmit ? 1 : 0.6,
184
+ }]}
185
+ onPress={onPress}
186
+ disabled={!canSubmit}
187
+ activeOpacity={0.8}
188
+ >
189
+ <AtomicText
190
+ type="titleLarge"
191
+ style={[{
192
+ fontWeight: "700",
193
+ fontSize: 17,
194
+ letterSpacing: 0.3,
195
+ }, { color: canSubmit ? textColor : 'rgba(0,0,0,0.3)' }]}
196
+ >
197
+ {title}
198
+ </AtomicText>
199
+ </TouchableOpacity>
200
+ </View>
201
+ );
@@ -6,208 +6,112 @@
6
6
  * Collects user feedback after they decline the paywall.
7
7
  */
8
8
 
9
- import React, { useMemo, useCallback } from "react";
10
- import { View, ScrollView, TouchableOpacity } from "react-native";
11
- import { AtomicText, AtomicIcon } from "@umituz/react-native-design-system/atoms";
9
+ import React, { useMemo } from "react";
10
+ import { View, ScrollView } from "react-native";
12
11
  import { useSafeAreaInsets } from "@umituz/react-native-design-system/safe-area";
13
- import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
14
12
  import { usePaywallFeedback } from "../../../../../presentation/hooks/feedback/usePaywallFeedback";
15
- import { FeedbackOption } from "./FeedbackOption";
16
13
  import type { PaywallFeedbackScreenProps, PaywallFeedbackTranslations } from "./PaywallFeedbackScreen.types";
14
+ import {
15
+ FeedbackCloseButton,
16
+ FeedbackHeader,
17
+ FeedbackOptionsList,
18
+ FeedbackSubmitButton,
19
+ } from "./PaywallFeedbackScreen.parts";
17
20
 
18
21
  // Re-export types for convenience
19
22
  export type { PaywallFeedbackTranslations, PaywallFeedbackScreenProps };
20
23
 
21
- const FEEDBACK_OPTION_IDS = [
22
- "too_expensive",
23
- "no_need",
24
- "trying_out",
25
- "technical_issues",
26
- "other",
27
- ] as const;
28
-
29
24
  export const PaywallFeedbackScreen: React.FC<PaywallFeedbackScreenProps> = React.memo(({
30
- translations,
31
- onClose,
32
- onSubmit,
25
+ translations,
26
+ onClose,
27
+ onSubmit,
33
28
  }) => {
34
- const tokens = useAppDesignTokens();
35
- const insets = useSafeAreaInsets();
36
-
37
- const {
38
- selectedReason,
39
- otherText,
40
- setOtherText,
41
- selectReason,
42
- handleSubmit,
43
- handleSkip,
44
- canSubmit,
45
- } = usePaywallFeedback({ onSubmit, onClose });
46
-
47
- const screenStyles = useMemo(() => createScreenStyles(tokens, insets), [tokens, insets]);
29
+ const insets = useSafeAreaInsets();
48
30
 
49
- const handleSkipPress = useCallback(() => {
50
- handleSkip();
51
- }, [handleSkip]);
31
+ const {
32
+ selectedReason,
33
+ otherText,
34
+ setOtherText,
35
+ selectReason,
36
+ handleSubmit,
37
+ handleSkip,
38
+ canSubmit,
39
+ } = usePaywallFeedback({ onSubmit, onClose });
52
40
 
53
- return (
54
- <View style={[screenStyles.container, { backgroundColor: tokens.colors.backgroundPrimary }]}>
55
- {/* Close button */}
56
- <TouchableOpacity
57
- onPress={handleSkipPress}
58
- style={[screenStyles.closeBtn, { backgroundColor: tokens.colors.surfaceSecondary, top: Math.max(insets.top, 12) }]}
59
- hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
60
- >
61
- <AtomicIcon name="close-outline" size="md" customColor={tokens.colors.textPrimary} />
62
- </TouchableOpacity>
41
+ const screenStyles = useMemo(() => createScreenStyles(insets), [insets]);
63
42
 
64
- {/* Scrollable content */}
65
- <ScrollView
66
- style={screenStyles.scrollContainer}
67
- contentContainerStyle={screenStyles.scrollContent}
68
- showsVerticalScrollIndicator={false}
69
- >
70
- {/* Header */}
71
- <View style={screenStyles.header}>
72
- <AtomicText
73
- type="headlineMedium"
74
- style={[screenStyles.title, { color: tokens.colors.textPrimary }]}
75
- >
76
- {translations.title}
77
- </AtomicText>
78
- {translations.subtitle && (
79
- <AtomicText
80
- type="bodyMedium"
81
- style={[screenStyles.subtitle, { color: tokens.colors.textSecondary }]}
82
- >
83
- {translations.subtitle}
84
- </AtomicText>
85
- )}
86
- </View>
43
+ return (
44
+ <View style={[screenStyles.container, { backgroundColor: 'white', opacity: 1 }]}>
45
+ {/* Close button */}
46
+ <FeedbackCloseButton
47
+ onPress={handleSkip}
48
+ topInset={insets.top}
49
+ backgroundColor="rgba(0,0,0,0.05)"
50
+ iconColor="#000"
51
+ />
87
52
 
88
- {/* Feedback options */}
89
- <View style={screenStyles.optionsContainer}>
90
- {FEEDBACK_OPTION_IDS.map((optionId) => {
91
- const isSelected = selectedReason === optionId;
92
- const isOther = optionId === "other";
93
- const showInput = isSelected && isOther;
53
+ {/* Scrollable content */}
54
+ <ScrollView
55
+ style={screenStyles.scrollContainer}
56
+ contentContainerStyle={screenStyles.scrollContent}
57
+ showsVerticalScrollIndicator={false}
58
+ >
59
+ {/* Header */}
60
+ <FeedbackHeader
61
+ title={translations.title}
62
+ subtitle={translations.subtitle}
63
+ titleColor="#000"
64
+ subtitleColor="#666"
65
+ style={screenStyles.header}
66
+ />
94
67
 
95
- return (
96
- <FeedbackOption
97
- key={optionId}
98
- isSelected={isSelected}
99
- text={translations.reasons[optionId]}
100
- showInput={showInput}
101
- placeholder={translations.otherPlaceholder}
102
- inputValue={otherText}
103
- onSelect={() => selectReason(optionId)}
104
- onChangeText={setOtherText}
105
- />
106
- );
107
- })}
108
- </View>
109
- </ScrollView>
110
-
111
- {/* Sticky footer - Submit button */}
112
- <View style={[screenStyles.footer, { paddingBottom: Math.max(insets.bottom, 16) }]}>
113
- <TouchableOpacity
114
- style={[
115
- screenStyles.submitButton,
116
- {
117
- backgroundColor: canSubmit ? tokens.colors.primary : tokens.colors.surfaceSecondary,
118
- opacity: canSubmit ? 1 : 0.6,
119
- }
120
- ]}
121
- onPress={handleSubmit}
122
- disabled={!canSubmit}
123
- activeOpacity={0.8}
124
- >
125
- <AtomicText
126
- type="titleLarge"
127
- style={[
128
- screenStyles.submitText,
129
- { color: canSubmit ? tokens.colors.onPrimary : tokens.colors.textDisabled }
130
- ]}
131
- >
132
- {translations.submit}
133
- </AtomicText>
134
- </TouchableOpacity>
135
- </View>
68
+ {/* Feedback options */}
69
+ <View style={screenStyles.optionsContainer}>
70
+ <FeedbackOptionsList
71
+ translations={translations}
72
+ selectedReason={selectedReason}
73
+ otherText={otherText}
74
+ onSelectReason={selectReason}
75
+ onSetOtherText={setOtherText}
76
+ />
136
77
  </View>
137
- );
78
+ </ScrollView>
79
+
80
+ {/* Submit button */}
81
+ <FeedbackSubmitButton
82
+ title={translations.submit}
83
+ canSubmit={canSubmit}
84
+ backgroundColor="#007AFF"
85
+ textColor="#FFF"
86
+ onPress={handleSubmit}
87
+ bottomInset={insets.bottom}
88
+ />
89
+ </View>
90
+ );
138
91
  });
139
92
 
140
93
  PaywallFeedbackScreen.displayName = "PaywallFeedbackScreen";
141
94
 
142
- const createScreenStyles = (
143
- tokens: {
144
- colors: { backgroundPrimary: string; border: string; onPrimary: string; textDisabled: string; surfaceSecondary: string; primary: string };
145
- spacing: { xl: number; sm: number; md: number; lg: number };
146
- },
147
- _insets: { top: number; bottom: number }
148
- ) => ({
149
- container: {
150
- flex: 1,
151
- opacity: 1,
152
- },
153
- closeBtn: {
154
- position: 'absolute' as const,
155
- top: 12,
156
- right: 12,
157
- width: 40,
158
- height: 40,
159
- borderRadius: 20,
160
- zIndex: 1000,
161
- justifyContent: 'center' as const,
162
- alignItems: 'center' as const,
163
- },
164
- scrollContainer: {
165
- flex: 1,
166
- },
167
- scrollContent: {
168
- paddingTop: 80,
169
- paddingBottom: 120,
170
- },
171
- header: {
172
- paddingHorizontal: tokens.spacing.xl,
173
- marginBottom: tokens.spacing.xl + 8,
174
- },
175
- title: {
176
- marginBottom: tokens.spacing.md + 4,
177
- },
178
- subtitle: {
179
- lineHeight: 24,
180
- },
181
- optionsContainer: {
182
- paddingHorizontal: tokens.spacing.xl,
183
- gap: tokens.spacing.md,
184
- },
185
- footer: {
186
- position: 'absolute' as const,
187
- bottom: 0,
188
- left: 0,
189
- right: 0,
190
- paddingHorizontal: tokens.spacing.xl,
191
- paddingTop: tokens.spacing.lg,
192
- paddingBottom: tokens.spacing.lg,
193
- backgroundColor: tokens.colors.backgroundPrimary,
194
- borderTopWidth: 1,
195
- borderTopColor: tokens.colors.border,
196
- opacity: 1,
197
- },
198
- submitButton: {
199
- borderRadius: 16,
200
- paddingVertical: 18,
201
- alignItems: 'center' as const,
202
- shadowColor: "#000",
203
- shadowOffset: { width: 0, height: 2 },
204
- shadowOpacity: 0.1,
205
- shadowRadius: 4,
206
- elevation: 3,
207
- },
208
- submitText: {
209
- fontWeight: "700" as const,
210
- fontSize: 17,
211
- letterSpacing: 0.3,
212
- },
95
+ // ============================================================================
96
+ // STYLES
97
+ // ============================================================================
98
+
99
+ const createScreenStyles = (insets: { top: number; bottom: number }) => ({
100
+ container: {
101
+ flex: 1,
102
+ },
103
+ scrollContainer: {
104
+ flex: 1,
105
+ },
106
+ scrollContent: {
107
+ paddingTop: 80,
108
+ paddingBottom: 120,
109
+ },
110
+ header: {
111
+ paddingHorizontal: 24,
112
+ marginBottom: 32,
113
+ },
114
+ optionsContainer: {
115
+ paddingHorizontal: 24,
116
+ },
213
117
  });