@umituz/react-native-subscription 2.2.11 → 2.2.12
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.12",
|
|
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",
|
|
@@ -58,4 +58,4 @@
|
|
|
58
58
|
"README.md",
|
|
59
59
|
"LICENSE"
|
|
60
60
|
]
|
|
61
|
-
}
|
|
61
|
+
}
|
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
|
+
};
|