@umituz/react-native-subscription 2.2.11 → 2.2.13
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.2.
|
|
3
|
+
"version": "2.2.13",
|
|
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
|
@@ -46,6 +46,10 @@ export {
|
|
|
46
46
|
resetSubscriptionService,
|
|
47
47
|
} from "./infrastructure/services/SubscriptionService";
|
|
48
48
|
|
|
49
|
+
// Feedback
|
|
50
|
+
export * from "./presentation/components/feedback/PaywallFeedbackModal";
|
|
51
|
+
export * from "./presentation/hooks/feedback/usePaywallFeedback";
|
|
52
|
+
|
|
49
53
|
// =============================================================================
|
|
50
54
|
// PRESENTATION LAYER - Hooks
|
|
51
55
|
// =============================================================================
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Paywall Feedback Modal
|
|
3
|
+
* Collects user feedback when declining paywall
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React, { useMemo } from "react";
|
|
7
|
+
import {
|
|
8
|
+
View,
|
|
9
|
+
Modal,
|
|
10
|
+
TouchableOpacity,
|
|
11
|
+
TouchableWithoutFeedback,
|
|
12
|
+
TextInput,
|
|
13
|
+
KeyboardAvoidingView,
|
|
14
|
+
} from "react-native";
|
|
15
|
+
import {
|
|
16
|
+
AtomicText,
|
|
17
|
+
} from "@umituz/react-native-design-system-atoms";
|
|
18
|
+
import { useAppDesignTokens } from "@umituz/react-native-design-system-theme";
|
|
19
|
+
import { useLocalization } from "@umituz/react-native-localization";
|
|
20
|
+
import { usePaywallFeedback } from "../../hooks/feedback/usePaywallFeedback";
|
|
21
|
+
import { createPaywallFeedbackStyles } from "./paywallFeedbackStyles";
|
|
22
|
+
|
|
23
|
+
const FEEDBACK_OPTIONS = [
|
|
24
|
+
{ id: "too_expensive", labelKey: "paywall.feedback.reasons.tooExpensive" },
|
|
25
|
+
{ id: "no_need", labelKey: "paywall.feedback.reasons.noNeed" },
|
|
26
|
+
{ id: "trying_out", labelKey: "paywall.feedback.reasons.tryingOut" },
|
|
27
|
+
{ id: "technical_issues", labelKey: "paywall.feedback.reasons.technicalIssues" },
|
|
28
|
+
{ id: "other", labelKey: "paywall.feedback.reasons.other" },
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
export interface PaywallFeedbackModalProps {
|
|
32
|
+
visible: boolean;
|
|
33
|
+
onClose: () => void;
|
|
34
|
+
onSubmit: (reason: string) => void;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const PaywallFeedbackModal: React.FC<PaywallFeedbackModalProps> = React.memo(({
|
|
38
|
+
visible,
|
|
39
|
+
onClose,
|
|
40
|
+
onSubmit,
|
|
41
|
+
}) => {
|
|
42
|
+
const { t } = useLocalization();
|
|
43
|
+
const tokens = useAppDesignTokens();
|
|
44
|
+
|
|
45
|
+
const {
|
|
46
|
+
selectedReason,
|
|
47
|
+
otherText,
|
|
48
|
+
setOtherText,
|
|
49
|
+
selectReason,
|
|
50
|
+
handleSubmit,
|
|
51
|
+
handleSkip,
|
|
52
|
+
canSubmit,
|
|
53
|
+
} = usePaywallFeedback({ onSubmit, onClose });
|
|
54
|
+
|
|
55
|
+
const styles = useMemo(
|
|
56
|
+
() => createPaywallFeedbackStyles(tokens, canSubmit),
|
|
57
|
+
[tokens, canSubmit],
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<Modal
|
|
62
|
+
visible={visible}
|
|
63
|
+
transparent
|
|
64
|
+
animationType="fade"
|
|
65
|
+
onRequestClose={handleSkip}
|
|
66
|
+
>
|
|
67
|
+
<TouchableWithoutFeedback onPress={handleSkip}>
|
|
68
|
+
<View style={styles.overlay}>
|
|
69
|
+
<KeyboardAvoidingView behavior="padding" style={styles.keyboardView}>
|
|
70
|
+
<TouchableWithoutFeedback>
|
|
71
|
+
<View style={styles.container}>
|
|
72
|
+
<View style={styles.header}>
|
|
73
|
+
<AtomicText style={styles.title}>
|
|
74
|
+
{t("paywall.feedback.title", { defaultValue: "Help us improve" })}
|
|
75
|
+
</AtomicText>
|
|
76
|
+
<AtomicText style={styles.subtitle}>
|
|
77
|
+
{t("paywall.feedback.subtitle", { defaultValue: "We'd love to know why you decided not to join Premium today." })}
|
|
78
|
+
</AtomicText>
|
|
79
|
+
</View>
|
|
80
|
+
|
|
81
|
+
<View style={styles.optionsContainer}>
|
|
82
|
+
{FEEDBACK_OPTIONS.map((option, index) => {
|
|
83
|
+
const isSelected = selectedReason === option.id;
|
|
84
|
+
const isLast = index === FEEDBACK_OPTIONS.length - 1;
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<View key={option.id}>
|
|
88
|
+
<TouchableOpacity
|
|
89
|
+
style={[
|
|
90
|
+
styles.optionRow,
|
|
91
|
+
isLast ? styles.optionRowLast : undefined,
|
|
92
|
+
]}
|
|
93
|
+
onPress={() => selectReason(option.id)}
|
|
94
|
+
activeOpacity={0.6}
|
|
95
|
+
>
|
|
96
|
+
<AtomicText
|
|
97
|
+
style={[
|
|
98
|
+
styles.optionText,
|
|
99
|
+
isSelected
|
|
100
|
+
? styles.optionTextSelected
|
|
101
|
+
: undefined,
|
|
102
|
+
]}
|
|
103
|
+
>
|
|
104
|
+
{t(option.labelKey, { defaultValue: option.id })}
|
|
105
|
+
</AtomicText>
|
|
106
|
+
|
|
107
|
+
<View
|
|
108
|
+
style={[
|
|
109
|
+
styles.radioButton,
|
|
110
|
+
isSelected
|
|
111
|
+
? styles.radioButtonSelected
|
|
112
|
+
: undefined,
|
|
113
|
+
]}
|
|
114
|
+
>
|
|
115
|
+
{isSelected && (
|
|
116
|
+
<View style={styles.radioButtonInner} />
|
|
117
|
+
)}
|
|
118
|
+
</View>
|
|
119
|
+
</TouchableOpacity>
|
|
120
|
+
|
|
121
|
+
{isSelected && option.id === "other" && (
|
|
122
|
+
<View style={styles.inputContainer}>
|
|
123
|
+
<TextInput
|
|
124
|
+
style={styles.textInput}
|
|
125
|
+
placeholder={t("paywall.feedback.otherPlaceholder", { defaultValue: "Tell us more..." })}
|
|
126
|
+
placeholderTextColor={tokens.colors.textTertiary}
|
|
127
|
+
multiline
|
|
128
|
+
maxLength={200}
|
|
129
|
+
value={otherText}
|
|
130
|
+
onChangeText={setOtherText}
|
|
131
|
+
autoFocus
|
|
132
|
+
/>
|
|
133
|
+
</View>
|
|
134
|
+
)}
|
|
135
|
+
</View>
|
|
136
|
+
);
|
|
137
|
+
})}
|
|
138
|
+
</View>
|
|
139
|
+
|
|
140
|
+
<View style={styles.footer}>
|
|
141
|
+
<TouchableOpacity
|
|
142
|
+
style={styles.submitButton}
|
|
143
|
+
onPress={handleSubmit}
|
|
144
|
+
disabled={!canSubmit}
|
|
145
|
+
activeOpacity={0.8}
|
|
146
|
+
>
|
|
147
|
+
<AtomicText style={styles.submitText}>
|
|
148
|
+
{t("paywall.feedback.submit", { defaultValue: "Submit" })}
|
|
149
|
+
</AtomicText>
|
|
150
|
+
</TouchableOpacity>
|
|
151
|
+
</View>
|
|
152
|
+
</View>
|
|
153
|
+
</TouchableWithoutFeedback>
|
|
154
|
+
</KeyboardAvoidingView>
|
|
155
|
+
</View>
|
|
156
|
+
</TouchableWithoutFeedback>
|
|
157
|
+
</Modal>
|
|
158
|
+
);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
PaywallFeedbackModal.displayName = "PaywallFeedbackModal";
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Paywall Feedback Styles
|
|
3
|
+
* Generates styles based on design tokens
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { StyleSheet } from "react-native";
|
|
7
|
+
import type { DesignTokens } from "@umituz/react-native-design-system-theme";
|
|
8
|
+
|
|
9
|
+
export const createPaywallFeedbackStyles = (
|
|
10
|
+
tokens: DesignTokens,
|
|
11
|
+
canSubmit: boolean
|
|
12
|
+
) =>
|
|
13
|
+
StyleSheet.create({
|
|
14
|
+
overlay: {
|
|
15
|
+
flex: 1,
|
|
16
|
+
backgroundColor: "rgba(0, 0, 0, 0.5)",
|
|
17
|
+
justifyContent: "center",
|
|
18
|
+
alignItems: "center",
|
|
19
|
+
padding: 24,
|
|
20
|
+
},
|
|
21
|
+
keyboardView: {
|
|
22
|
+
width: "100%",
|
|
23
|
+
alignItems: "center",
|
|
24
|
+
},
|
|
25
|
+
container: {
|
|
26
|
+
width: "100%",
|
|
27
|
+
maxWidth: 340,
|
|
28
|
+
backgroundColor: tokens.colors.surface,
|
|
29
|
+
borderRadius: 24,
|
|
30
|
+
overflow: "hidden",
|
|
31
|
+
padding: 24,
|
|
32
|
+
},
|
|
33
|
+
header: {
|
|
34
|
+
alignItems: "center",
|
|
35
|
+
marginBottom: 24,
|
|
36
|
+
},
|
|
37
|
+
title: {
|
|
38
|
+
fontSize: 20,
|
|
39
|
+
fontWeight: "700",
|
|
40
|
+
color: tokens.colors.textPrimary,
|
|
41
|
+
marginBottom: 8,
|
|
42
|
+
textAlign: "center",
|
|
43
|
+
},
|
|
44
|
+
subtitle: {
|
|
45
|
+
fontSize: 14,
|
|
46
|
+
color: tokens.colors.textSecondary,
|
|
47
|
+
textAlign: "center",
|
|
48
|
+
lineHeight: 20,
|
|
49
|
+
},
|
|
50
|
+
optionsContainer: {
|
|
51
|
+
gap: 0,
|
|
52
|
+
marginBottom: 24,
|
|
53
|
+
backgroundColor: tokens.colors.surfaceSecondary,
|
|
54
|
+
borderRadius: 16,
|
|
55
|
+
overflow: "hidden",
|
|
56
|
+
},
|
|
57
|
+
optionRow: {
|
|
58
|
+
flexDirection: "row",
|
|
59
|
+
alignItems: "center",
|
|
60
|
+
justifyContent: "space-between",
|
|
61
|
+
padding: 16,
|
|
62
|
+
borderBottomWidth: 1,
|
|
63
|
+
borderBottomColor: tokens.colors.border,
|
|
64
|
+
},
|
|
65
|
+
optionRowLast: {
|
|
66
|
+
borderBottomWidth: 0,
|
|
67
|
+
},
|
|
68
|
+
optionText: {
|
|
69
|
+
fontSize: 15,
|
|
70
|
+
color: tokens.colors.textSecondary,
|
|
71
|
+
flex: 1,
|
|
72
|
+
marginRight: 16,
|
|
73
|
+
},
|
|
74
|
+
optionTextSelected: {
|
|
75
|
+
color: tokens.colors.textPrimary,
|
|
76
|
+
fontWeight: "600",
|
|
77
|
+
},
|
|
78
|
+
radioButton: {
|
|
79
|
+
width: 20,
|
|
80
|
+
height: 20,
|
|
81
|
+
borderRadius: 10,
|
|
82
|
+
borderWidth: 2,
|
|
83
|
+
borderColor: tokens.colors.border,
|
|
84
|
+
justifyContent: "center",
|
|
85
|
+
alignItems: "center",
|
|
86
|
+
},
|
|
87
|
+
radioButtonSelected: {
|
|
88
|
+
borderColor: tokens.colors.primary,
|
|
89
|
+
},
|
|
90
|
+
radioButtonInner: {
|
|
91
|
+
width: 10,
|
|
92
|
+
height: 10,
|
|
93
|
+
borderRadius: 5,
|
|
94
|
+
backgroundColor: tokens.colors.primary,
|
|
95
|
+
},
|
|
96
|
+
inputContainer: {
|
|
97
|
+
padding: 16,
|
|
98
|
+
paddingTop: 0,
|
|
99
|
+
},
|
|
100
|
+
textInput: {
|
|
101
|
+
backgroundColor: tokens.colors.surface,
|
|
102
|
+
borderRadius: 12,
|
|
103
|
+
padding: 12,
|
|
104
|
+
fontSize: 14,
|
|
105
|
+
color: tokens.colors.textPrimary,
|
|
106
|
+
minHeight: 80,
|
|
107
|
+
textAlignVertical: "top",
|
|
108
|
+
},
|
|
109
|
+
footer: {
|
|
110
|
+
gap: 12,
|
|
111
|
+
},
|
|
112
|
+
submitButton: {
|
|
113
|
+
backgroundColor: canSubmit ? tokens.colors.primary : tokens.colors.surfaceSecondary,
|
|
114
|
+
borderRadius: 12,
|
|
115
|
+
paddingVertical: 14,
|
|
116
|
+
alignItems: "center",
|
|
117
|
+
opacity: canSubmit ? 1 : 0.7,
|
|
118
|
+
},
|
|
119
|
+
submitText: {
|
|
120
|
+
color: canSubmit ? tokens.colors.onPrimary : tokens.colors.textDisabled,
|
|
121
|
+
fontWeight: "600",
|
|
122
|
+
fontSize: 16,
|
|
123
|
+
},
|
|
124
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { useState, useCallback } from "react";
|
|
2
|
+
|
|
3
|
+
interface UsePaywallFeedbackProps {
|
|
4
|
+
onSubmit: (reason: string) => void;
|
|
5
|
+
onClose: () => void;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const usePaywallFeedback = ({
|
|
9
|
+
onSubmit,
|
|
10
|
+
onClose,
|
|
11
|
+
}: UsePaywallFeedbackProps) => {
|
|
12
|
+
const [selectedReason, setSelectedReason] = useState<string | null>(null);
|
|
13
|
+
const [otherText, setOtherText] = useState("");
|
|
14
|
+
|
|
15
|
+
const handleSubmit = useCallback(() => {
|
|
16
|
+
if (!selectedReason) return;
|
|
17
|
+
|
|
18
|
+
const finalReason =
|
|
19
|
+
selectedReason === "other" && otherText.trim().length > 0
|
|
20
|
+
? `other: ${otherText.trim()}`
|
|
21
|
+
: selectedReason;
|
|
22
|
+
|
|
23
|
+
onSubmit(finalReason);
|
|
24
|
+
setSelectedReason(null);
|
|
25
|
+
setOtherText("");
|
|
26
|
+
onClose();
|
|
27
|
+
}, [selectedReason, otherText, onSubmit, onClose]);
|
|
28
|
+
|
|
29
|
+
const handleSkip = useCallback(() => {
|
|
30
|
+
setSelectedReason(null);
|
|
31
|
+
setOtherText("");
|
|
32
|
+
onClose();
|
|
33
|
+
}, [onClose]);
|
|
34
|
+
|
|
35
|
+
const selectReason = useCallback((id: string) => {
|
|
36
|
+
setSelectedReason(id);
|
|
37
|
+
}, []);
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
selectedReason,
|
|
41
|
+
otherText,
|
|
42
|
+
setOtherText,
|
|
43
|
+
selectReason,
|
|
44
|
+
handleSubmit,
|
|
45
|
+
handleSkip,
|
|
46
|
+
canSubmit: !!selectedReason,
|
|
47
|
+
};
|
|
48
|
+
};
|