@umituz/react-native-subscription 2.2.10 → 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.10",
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",
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
+ };