diff --git a/app/Gemfile.lock b/app/Gemfile.lock index 1b67015b5..903950d00 100644 --- a/app/Gemfile.lock +++ b/app/Gemfile.lock @@ -23,7 +23,7 @@ GEM artifactory (3.0.17) atomos (0.1.3) aws-eventstream (1.4.0) - aws-partitions (1.1201.0) + aws-partitions (1.1204.0) aws-sdk-core (3.241.3) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.992.0) @@ -254,7 +254,7 @@ GEM optparse (0.8.1) os (1.1.4) plist (3.7.2) - prism (1.7.0) + prism (1.8.0) public_suffix (4.0.7) racc (1.8.1) rake (13.3.1) diff --git a/app/src/components/FeedbackModalScreen.tsx b/app/src/components/AlertModal.tsx similarity index 94% rename from app/src/components/FeedbackModalScreen.tsx rename to app/src/components/AlertModal.tsx index 96c716ef9..55909ecab 100644 --- a/app/src/components/FeedbackModalScreen.tsx +++ b/app/src/components/AlertModal.tsx @@ -30,7 +30,7 @@ const ModalBackDrop = styled(View, { height: '100%', }); -export interface FeedbackModalScreenParams { +export interface AlertModalParams { titleText: string; bodyText: string; buttonText: string; @@ -41,13 +41,13 @@ export interface FeedbackModalScreenParams { preventDismiss?: boolean; } -interface FeedbackModalScreenProps { +interface AlertModalProps { visible: boolean; - modalParams: FeedbackModalScreenParams | null; + modalParams: AlertModalParams | null; onHideModal?: () => void; } -const FeedbackModalScreen: React.FC = ({ +const AlertModal: React.FC = ({ visible, modalParams, onHideModal, @@ -145,4 +145,4 @@ const styles = StyleSheet.create({ }, }); -export default FeedbackModalScreen; +export default AlertModal; diff --git a/app/src/components/FeedbackModal.tsx b/app/src/components/FeedbackModal.tsx index 7e06cd9cc..4a4e7ca60 100644 --- a/app/src/components/FeedbackModal.tsx +++ b/app/src/components/FeedbackModal.tsx @@ -2,24 +2,25 @@ // SPDX-License-Identifier: BUSL-1.1 // NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. -import React, { useState } from 'react'; -import { Alert, Modal, StyleSheet, Text, TextInput, View } from 'react-native'; +import React from 'react'; +import { Modal, StyleSheet, Text, View } from 'react-native'; import { Button, XStack, YStack } from 'tamagui'; import { Caption } from '@selfxyz/mobile-sdk-alpha/components'; import { - black, - slate400, white, zinc800, zinc900, } from '@selfxyz/mobile-sdk-alpha/constants/colors'; import { advercase, dinot } from '@selfxyz/mobile-sdk-alpha/constants/fonts'; +import ModalClose from '@/assets/icons/modal_close.svg'; +import { openSupportForm } from '@/services/support'; + interface FeedbackModalProps { visible: boolean; onClose: () => void; - onSubmit: ( + onSubmit?: ( feedback: string, category: string, name?: string, @@ -27,65 +28,10 @@ interface FeedbackModalProps { ) => void; } -const FeedbackModal: React.FC = ({ - visible, - onClose, - onSubmit, -}) => { - const [feedback, setFeedback] = useState(''); - const [category, setCategory] = useState('general'); - const [name, setName] = useState(''); - const [email, setEmail] = useState(''); - const [isSubmitting, setIsSubmitting] = useState(false); - - const categories = [ - { value: 'general', label: 'General Feedback' }, - { value: 'bug', label: 'Bug Report' }, - { value: 'feature', label: 'Feature Request' }, - { value: 'ui', label: 'UI/UX Issue' }, - ]; - - const handleSubmit = async () => { - if (!feedback.trim()) { - Alert.alert('Error', 'Please enter your feedback'); - return; - } - - setIsSubmitting(true); - try { - await onSubmit( - feedback.trim(), - category, - name.trim() || undefined, - email.trim() || undefined, - ); - setFeedback(''); - setCategory('general'); - setName(''); - setEmail(''); - onClose(); - Alert.alert('Success', 'Thank you for your feedback!'); - } catch (error) { - console.error('Error submitting feedback:', error); - Alert.alert('Error', 'Failed to submit feedback. Please try again.'); - } finally { - setIsSubmitting(false); - } - }; - - const handleClose = () => { - if (feedback.trim() || name.trim() || email.trim()) { - Alert.alert( - 'Discard Feedback?', - 'You have unsaved feedback. Are you sure you want to close?', - [ - { text: 'Cancel', style: 'cancel' }, - { text: 'Discard', style: 'destructive', onPress: onClose }, - ], - ); - } else { - onClose(); - } +const FeedbackModal: React.FC = ({ visible, onClose }) => { + const handleSupportForm = async () => { + await openSupportForm(); + onClose(); }; return ( @@ -93,93 +39,33 @@ const FeedbackModal: React.FC = ({ visible={visible} animationType="slide" transparent={true} - onRequestClose={handleClose} + onRequestClose={onClose} > Send Feedback - + - - Category - - {categories.map(cat => ( - - ))} - - - - - - Contact Information (Optional) + + + Have feedback, suggestions, or found a bug? + + + Fill out our feedback form and we'll review it as soon as + possible. - - - - - - - - Your Feedback - @@ -201,7 +87,6 @@ const styles = StyleSheet.create({ borderRadius: 16, width: '100%', maxWidth: 400, - maxHeight: '80%', borderWidth: 1, borderColor: zinc800, }, @@ -211,22 +96,12 @@ const styles = StyleSheet.create({ fontWeight: '600', color: white, }, - label: { + messageText: { fontFamily: dinot, color: white, - fontSize: 14, - fontWeight: '500', - }, - textInput: { - backgroundColor: black, - borderWidth: 1, - borderColor: zinc800, - borderRadius: 8, - padding: 12, - color: white, - fontSize: 16, - fontFamily: dinot, - minHeight: 120, + fontSize: 15, + textAlign: 'center', + lineHeight: 22, }, }); diff --git a/app/src/consts/links.ts b/app/src/consts/links.ts index 21702adf4..c5a427c47 100644 --- a/app/src/consts/links.ts +++ b/app/src/consts/links.ts @@ -33,6 +33,8 @@ export const referralBaseUrl = 'https://referral.self.xyz'; export const selfLogoReverseUrl = 'https://storage.googleapis.com/self-logo-reverse/Self%20Logomark%20Reverse.png'; export const selfUrl = 'https://self.xyz'; +export const supportFormUrl = + 'https://hail-jonquil-ef8.notion.site/2b057801cd128041985dfd6e1722eca1'; export const supportedBiometricIdsUrl = 'https://docs.self.xyz/use-self/self-map-countries-list'; export const telegramUrl = 'https://t.me/selfxyz'; diff --git a/app/src/hooks/useFeedbackModal.ts b/app/src/hooks/useFeedbackModal.ts index a2df0da32..0fdaf651c 100644 --- a/app/src/hooks/useFeedbackModal.ts +++ b/app/src/hooks/useFeedbackModal.ts @@ -9,7 +9,7 @@ import { showFeedbackWidget, } from '@sentry/react-native'; -import type { FeedbackModalScreenParams } from '@/components/FeedbackModalScreen'; +import type { AlertModalParams } from '@/components/AlertModal'; import { captureFeedback } from '@/config/sentry'; export type FeedbackType = 'button' | 'widget' | 'custom'; @@ -18,8 +18,7 @@ export const useFeedbackModal = () => { const timeoutRef = useRef | null>(null); const [isVisible, setIsVisible] = useState(false); const [isModalVisible, setIsModalVisible] = useState(false); - const [modalParams, setModalParams] = - useState(null); + const [modalParams, setModalParams] = useState(null); const showFeedbackModal = useCallback((type: FeedbackType = 'button') => { if (timeoutRef.current) { @@ -81,7 +80,7 @@ export const useFeedbackModal = () => { setIsVisible(false); }, []); - const showModal = useCallback((params: FeedbackModalScreenParams) => { + const showModal = useCallback((params: AlertModalParams) => { setModalParams(params); setIsModalVisible(true); }, []); diff --git a/app/src/layouts/SimpleScrolledTitleLayout.tsx b/app/src/layouts/SimpleScrolledTitleLayout.tsx index 905d2f6a9..753896b18 100644 --- a/app/src/layouts/SimpleScrolledTitleLayout.tsx +++ b/app/src/layouts/SimpleScrolledTitleLayout.tsx @@ -35,20 +35,26 @@ export default function SimpleScrolledTitleLayout({ footer, }: DetailListProps) { const insets = useSafeAreaInsets(); + const dismissBottomPadding = Math.min(16, insets.bottom); return ( - + {title} {header} - + {children} {footer && ( - + {footer} )} @@ -60,8 +66,8 @@ export default function SimpleScrolledTitleLayout({ {secondaryButtonText} )} - {/* Anchor the Dismiss button to bottom with only safe area padding */} - + {/* Anchor the Dismiss button to bottom with sane spacing */} + Dismiss diff --git a/app/src/providers/feedbackProvider.tsx b/app/src/providers/feedbackProvider.tsx index e4668679b..4f8381992 100644 --- a/app/src/providers/feedbackProvider.tsx +++ b/app/src/providers/feedbackProvider.tsx @@ -5,9 +5,9 @@ import type { ReactNode } from 'react'; import React, { createContext, useContext } from 'react'; +import type { AlertModalParams } from '@/components/AlertModal'; +import AlertModal from '@/components/AlertModal'; import FeedbackModal from '@/components/FeedbackModal'; -import type { FeedbackModalScreenParams } from '@/components/FeedbackModalScreen'; -import FeedbackModalScreen from '@/components/FeedbackModalScreen'; import type { FeedbackType } from '@/hooks/useFeedbackModal'; import { useFeedbackModal } from '@/hooks/useFeedbackModal'; @@ -19,7 +19,7 @@ interface FeedbackContextType { name?: string, email?: string, ) => Promise; - showModal: (params: FeedbackModalScreenParams) => void; + showModal: (params: AlertModalParams) => void; } const FeedbackContext = createContext( @@ -50,13 +50,9 @@ export const FeedbackProvider: React.FC = ({ > {children} - + - ; @@ -61,9 +60,8 @@ interface SocialButtonProps { href: string; } -const emailFeedback = 'support@self.xyz'; // Avoid importing RootStackParamList; we only need string route names plus a few literals -type RouteOption = string | 'share' | 'email_feedback' | 'ManageDocuments'; +type RouteOption = string | 'share' | 'support_form' | 'ManageDocuments'; const storeURL = Platform.OS === 'ios' ? appStoreUrl : playStoreUrl; @@ -79,7 +77,7 @@ const routes = [Lock, 'Reveal recovery phrase', 'ShowRecoveryPhrase'], [Cloud, 'Cloud backup', 'CloudBackupSettings'], [Settings2 as React.FC, 'Proof settings', 'ProofSettings'], - [Feedback, 'Send feedback', 'email_feedback'], + [Feedback, 'Get support', 'support_form'], [ShareIcon, 'Share Self app', 'share'], [ FileText as React.FC, @@ -90,7 +88,7 @@ const routes = : ([ [Data, 'View document info', 'DocumentDataInfo'], [Settings2 as React.FC, 'Proof settings', 'ProofSettings'], - [Feedback, 'Send feeback', 'email_feedback'], + [Feedback, 'Get support', 'support_form'], [ FileText as React.FC, 'Manage ID documents', @@ -222,32 +220,17 @@ const SettingsScreen: React.FC = () => { ); break; - case 'email_feedback': - const subject = 'SELF App Feedback'; - const deviceInfo = [ - ['device', `${Platform.OS}@${Platform.Version}`], - ['app', `v${version}`], - [ - 'locales', - getLocales() - .map(locale => `${locale.languageCode}-${locale.countryCode}`) - .join(','), - ], - ['country', getCountry()], - ['tz', getTimeZone()], - ['ts', new Date()], - ['origin', 'settings/feedback'], - ] as [string, string][]; - - const body = ` ---- -${deviceInfo.map(([k, v]) => `${k}=${v}`).join('; ')} ----`; - await Linking.openURL( - `mailto:${emailFeedback}?subject=${encodeURIComponent( - subject, - )}&body=${encodeURIComponent(body)}`, - ); + case 'support_form': + try { + await openSupportForm(); + } catch (error) { + console.warn( + 'SettingsScreen: failed to open support form:', + error instanceof Error ? error.message : String(error), + ); + // Error is already handled and displayed to user in openSupportForm, + // but we log here for debugging purposes + } break; case 'ManageDocuments': diff --git a/app/src/screens/documents/scanning/DocumentNFCScanScreen.tsx b/app/src/screens/documents/scanning/DocumentNFCScanScreen.tsx index 7b5b3cb47..72f1845aa 100644 --- a/app/src/screens/documents/scanning/DocumentNFCScanScreen.tsx +++ b/app/src/screens/documents/scanning/DocumentNFCScanScreen.tsx @@ -73,7 +73,11 @@ import { setNfcScanningActive, trackNfcEvent, } from '@/services/analytics'; -import { sendFeedbackEmail } from '@/services/email'; +import { + openSupportForm, + SUPPORT_FORM_BUTTON_TEXT, + SUPPORT_FORM_MESSAGE, +} from '@/services/support'; const emitter = Platform.OS === 'android' @@ -170,10 +174,7 @@ const DocumentNFCScanScreen: React.FC = () => { }); const onReportIssue = useCallback(() => { - sendFeedbackEmail({ - message: 'User reported an issue from NFC scan screen', - origin: 'passport/nfc', - }); + openSupportForm(); }, []); const openErrorModal = useCallback( @@ -191,14 +192,10 @@ const DocumentNFCScanScreen: React.FC = () => { showModal({ titleText: 'NFC Scan Error', bodyText: message, - buttonText: 'Report Issue', + buttonText: SUPPORT_FORM_BUTTON_TEXT, secondaryButtonText: 'Help', preventDismiss: false, - onButtonPress: () => - sendFeedbackEmail({ - message: sanitizeErrorMessage(message), - origin: 'passport/nfc', - }), + onButtonPress: openSupportForm, onSecondaryButtonPress: goToNFCTrouble, onModalDismiss: () => {}, }); @@ -426,7 +423,7 @@ const DocumentNFCScanScreen: React.FC = () => { }); openErrorModal(message); // We deliberately avoid opening any external feedback widgets here; - // users can send feedback via the email action in the modal. + // users can request support via the support form action in the modal. } finally { if (scanTimeoutRef.current) { clearTimeout(scanTimeoutRef.current); @@ -612,6 +609,9 @@ const DocumentNFCScanScreen: React.FC = () => { )} + + {SUPPORT_FORM_MESSAGE} + { Cancel - Report Issue + {SUPPORT_FORM_BUTTON_TEXT} diff --git a/app/src/screens/documents/scanning/DocumentNFCTroubleScreen.tsx b/app/src/screens/documents/scanning/DocumentNFCTroubleScreen.tsx index cbac34b45..ab7d76326 100644 --- a/app/src/screens/documents/scanning/DocumentNFCTroubleScreen.tsx +++ b/app/src/screens/documents/scanning/DocumentNFCTroubleScreen.tsx @@ -16,7 +16,7 @@ import { useFeedbackAutoHide } from '@/hooks/useFeedbackAutoHide'; import useHapticNavigation from '@/hooks/useHapticNavigation'; import SimpleScrolledTitleLayout from '@/layouts/SimpleScrolledTitleLayout'; import { flushAllAnalytics } from '@/services/analytics'; -import { sendFeedbackEmail } from '@/services/email'; +import { openSupportForm, SUPPORT_FORM_BUTTON_TEXT } from '@/services/support'; const tips: TipProps[] = [ { @@ -71,20 +71,9 @@ const DocumentNFCTroubleScreen: React.FC = () => { secondaryButtonText="Open NFC Options" onSecondaryButtonPress={goToNFCMethodSelection} footer={ - // Add top padding before buttons and normalize spacing - - - sendFeedbackEmail({ - message: 'User reported an issue from NFC trouble screen', - origin: 'passport/nfc-trouble', - }) - } - style={{ marginBottom: 0 }} - > - Report Issue - - + + {SUPPORT_FORM_BUTTON_TEXT} + } > = ({ route }) => { const onNotifyMe = async () => { try { - await sendCountrySupportNotification({ - countryName, - countryCode: countryCode !== 'Unknown' ? countryCode : '', - documentCategory: route.params?.documentCategory, - }); + await openSupportForm(); } catch (error) { - console.error('Failed to open email client:', error); + console.error('Failed to open support form:', error); } }; @@ -101,13 +101,11 @@ const ComingSoonScreen: React.FC = ({ route }) => { return ( - - + + = ({ route }) => { textAlign: 'center', color: black, marginBottom: 16, + paddingTop: 10, }} > Coming Soon @@ -150,7 +149,7 @@ const ComingSoonScreen: React.FC = ({ route }) => { paddingHorizontal: 10, }} > - Sign up for live updates. + {SUPPORT_FORM_COMING_SOON_MESSAGE} @@ -158,13 +157,14 @@ const ComingSoonScreen: React.FC = ({ route }) => { gap={16} backgroundColor={white} paddingHorizontal={20} - paddingVertical={20} + paddingTop={20} + paddingBottom={20} > - Sign up for updates + {SUPPORT_FORM_COMING_SOON_BUTTON_TEXT} { }; const handleCopyCode = async () => { - if (!code || code === DASH_CODE) { + if (isLoading || isCopied || !code || code === DASH_CODE) { return; } @@ -225,14 +225,18 @@ const StarfallPushCodeScreen: React.FC = () => { diff --git a/app/src/screens/verification/QRCodeTroubleScreen.tsx b/app/src/screens/verification/QRCodeTroubleScreen.tsx index d7c12064f..054c8e26d 100644 --- a/app/src/screens/verification/QRCodeTroubleScreen.tsx +++ b/app/src/screens/verification/QRCodeTroubleScreen.tsx @@ -3,8 +3,9 @@ // NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. import React, { useEffect } from 'react'; +import { View } from 'tamagui'; -import { Caption } from '@selfxyz/mobile-sdk-alpha/components'; +import { Caption, SecondaryButton } from '@selfxyz/mobile-sdk-alpha/components'; import { slate500 } from '@selfxyz/mobile-sdk-alpha/constants/colors'; import type { TipProps } from '@/components/Tips'; @@ -12,6 +13,11 @@ import Tips from '@/components/Tips'; import useHapticNavigation from '@/hooks/useHapticNavigation'; import SimpleScrolledTitleLayout from '@/layouts/SimpleScrolledTitleLayout'; import { flushAllAnalytics } from '@/services/analytics'; +import { + openSupportForm, + SUPPORT_FORM_BUTTON_TEXT, + SUPPORT_FORM_TIP_MESSAGE, +} from '@/services/support'; const tips: TipProps[] = [ { @@ -39,7 +45,7 @@ const tips: TipProps[] = [ const tipsDeeplink: TipProps[] = [ { title: 'Coming from another app/website?', - body: 'Please contact the support, a telegram group is available in the options menu.', + body: SUPPORT_FORM_TIP_MESSAGE, }, ]; @@ -55,12 +61,19 @@ const QRCodeTrouble: React.FC = () => { + {SUPPORT_FORM_BUTTON_TEXT} + + } > - + Here are some tips to help you successfully scan the QR code: - - + + + + ); }; diff --git a/app/src/services/email.ts b/app/src/services/email.ts deleted file mode 100644 index 6e086e34c..000000000 --- a/app/src/services/email.ts +++ /dev/null @@ -1,119 +0,0 @@ -// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc. -// SPDX-License-Identifier: BUSL-1.1 -// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. - -import { Linking, Platform } from 'react-native'; -import { getCountry, getLocales, getTimeZone } from 'react-native-localize'; - -import { sanitizeErrorMessage } from '@selfxyz/mobile-sdk-alpha/utils/utils'; - -import { version } from '../../package.json'; - -interface SendFeedbackEmailOptions { - message: string; - origin: string; - subject?: string; - recipient?: string; -} - -/** - * Sends a notification email requesting support for a specific country - * @param options Configuration for the country support notification email - */ -export const sendCountrySupportNotification = async ({ - countryName, - countryCode, - documentCategory, - subject = `Country Support Request: ${countryName}`, - recipient = 'support@self.xyz', -}: SendCountrySupportNotificationOptions): Promise => { - const deviceInfo = [ - ['device', `${Platform.OS}@${Platform.Version}`], - ['app', `v${version}`], - [ - 'locales', - getLocales() - .map(locale => `${locale.languageCode}-${locale.countryCode}`) - .join(','), - ], - ['userCountry', getCountry()], - ['requestedCountry', countryCode || 'Unknown'], - ['documentCategory', documentCategory || 'Unknown'], - ['tz', getTimeZone()], - ['ts', new Date().toISOString()], - ['origin', 'coming_soon_screen'], - ] as [string, string][]; - - const documentTypeText = - documentCategory === 'id_card' - ? 'ID cards' - : documentCategory === 'passport' - ? 'passports' - : 'documents'; - - const body = `Hi SELF Team, - -I would like to request support for ${countryName} ${documentTypeText} in the SELF app. Please notify me when support becomes available. - -Additional comments (optional): - - ---- -Technical Details (do not modify): -${deviceInfo.map(([k, v]) => `${k}=${v}`).join('\n')} ----`; - - await Linking.openURL( - `mailto:${recipient}?subject=${encodeURIComponent( - subject, - )}&body=${encodeURIComponent(body)}`, - ); -}; - -interface SendCountrySupportNotificationOptions { - countryName: string; - countryCode?: string; - documentCategory?: string; - subject?: string; - recipient?: string; -} - -/** - * Sends a feedback email with device information and user message - * @param options Configuration for the feedback email - */ -export const sendFeedbackEmail = async ({ - message, - origin, - subject = 'SELF App Feedback', - recipient = 'support@self.xyz', -}: SendFeedbackEmailOptions): Promise => { - const deviceInfo = [ - ['device', `${Platform.OS}@${Platform.Version}`], - ['app', `v${version}`], - [ - 'locales', - getLocales() - .map(locale => `${locale.languageCode}-${locale.countryCode}`) - .join(','), - ], - ['country', getCountry()], - ['tz', getTimeZone()], - ['ts', new Date().toISOString()], - ['origin', origin], - ['error', sanitizeErrorMessage(message)], - ] as [string, string][]; - - const body = `Please describe the issue you're experiencing: - ---- -Technical Details (do not modify): -${deviceInfo.map(([k, v]) => `${k}=${v}`).join('\n')} ----`; - - await Linking.openURL( - `mailto:${recipient}?subject=${encodeURIComponent( - subject, - )}&body=${encodeURIComponent(body)}`, - ); -}; diff --git a/app/src/services/support.ts b/app/src/services/support.ts new file mode 100644 index 000000000..e4a7f8225 --- /dev/null +++ b/app/src/services/support.ts @@ -0,0 +1,42 @@ +// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc. +// SPDX-License-Identifier: BUSL-1.1 +// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. + +import { Alert, Linking } from 'react-native'; + +import { supportFormUrl } from '@/consts/links'; + +export const SUPPORT_FORM_BUTTON_TEXT = 'Send feedback'; + +export const SUPPORT_FORM_COMING_SOON_BUTTON_TEXT = 'Let us know'; + +export const SUPPORT_FORM_COMING_SOON_MESSAGE = + 'Want your document supported? Let us know.'; + +export const SUPPORT_FORM_MESSAGE = 'Have feedback? Please fill out our form.'; + +export const SUPPORT_FORM_TIP_MESSAGE = 'Have feedback? Let us know.'; + +export const openSupportForm = async (): Promise => { + try { + const canOpen = await Linking.canOpenURL(supportFormUrl); + if (canOpen) { + await Linking.openURL(supportFormUrl); + } else { + console.warn('Cannot open support form URL - no handler available'); + Alert.alert( + 'Unable to Open Link', + 'No app is available to open the support form. Please try again using a web browser.', + ); + } + } catch (error) { + console.error( + 'Failed to open support form:', + error instanceof Error ? error.message : String(error), + ); + Alert.alert( + 'Error', + 'Unable to open support form. Please try again later or contact support through another method.', + ); + } +};