Replace email support with Notion form (#1613)

* Replace email support with Discord

* Add Discord support prompts

* Remove command log

* formatting, agent feedback

* update strings

* save wip

* fix button color

* update text and change support from discord to notion support form

* remove settings support form text

* rename component and update feedback modal to redirect to users to notion form

* formatting

* update text
This commit is contained in:
Justin Hernandez
2026-01-14 18:14:27 -08:00
committed by GitHub
parent f8179c92ba
commit 544d60f860
15 changed files with 169 additions and 379 deletions

View File

@@ -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)

View File

@@ -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<FeedbackModalScreenProps> = ({
const AlertModal: React.FC<AlertModalProps> = ({
visible,
modalParams,
onHideModal,
@@ -145,4 +145,4 @@ const styles = StyleSheet.create({
},
});
export default FeedbackModalScreen;
export default AlertModal;

View File

@@ -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<FeedbackModalProps> = ({
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<FeedbackModalProps> = ({ visible, onClose }) => {
const handleSupportForm = async () => {
await openSupportForm();
onClose();
};
return (
@@ -93,93 +39,33 @@ const FeedbackModal: React.FC<FeedbackModalProps> = ({
visible={visible}
animationType="slide"
transparent={true}
onRequestClose={handleClose}
onRequestClose={onClose}
>
<View style={styles.overlay}>
<View style={styles.modalContainer}>
<YStack gap="$4" padding="$4">
<XStack justifyContent="space-between" alignItems="center">
<Text style={styles.title}>Send Feedback</Text>
<Button
size="$2"
variant="outlined"
onPress={handleClose}
disabled={isSubmitting}
>
</Button>
<ModalClose onPress={onClose} />
</XStack>
<YStack gap="$2">
<Caption style={styles.label}>Category</Caption>
<XStack gap="$2" flexWrap="wrap">
{categories.map(cat => (
<Button
key={cat.value}
size="$2"
backgroundColor={
category === cat.value ? white : 'transparent'
}
color={category === cat.value ? black : white}
borderColor={white}
onPress={() => setCategory(cat.value)}
disabled={isSubmitting}
>
{cat.label}
</Button>
))}
</XStack>
</YStack>
<YStack gap="$2">
<Caption style={styles.label}>
Contact Information (Optional)
<YStack gap="$3" alignItems="center" paddingVertical="$2">
<Caption style={styles.messageText}>
Have feedback, suggestions, or found a bug?
</Caption>
<Caption style={styles.messageText}>
Fill out our feedback form and we'll review it as soon as
possible.
</Caption>
<XStack gap="$2">
<TextInput
style={[styles.textInput, { flex: 1, minHeight: 48 }]}
placeholder="Name"
placeholderTextColor={slate400}
value={name}
onChangeText={setName}
editable={!isSubmitting}
/>
<TextInput
style={[styles.textInput, { flex: 1, minHeight: 48 }]}
placeholder="Email"
placeholderTextColor={slate400}
value={email}
onChangeText={setEmail}
keyboardType="email-address"
autoCapitalize="none"
editable={!isSubmitting}
/>
</XStack>
</YStack>
<YStack gap="$2">
<Caption style={styles.label}>Your Feedback</Caption>
<TextInput
style={styles.textInput}
placeholder="Tell us what you think, report a bug, or suggest a feature..."
placeholderTextColor={slate400}
value={feedback}
onChangeText={setFeedback}
multiline
numberOfLines={6}
textAlignVertical="top"
editable={!isSubmitting}
/>
</YStack>
<Button
size="$4"
backgroundColor={white}
color={black}
onPress={handleSubmit}
disabled={isSubmitting || !feedback.trim()}
color="$black"
onPress={handleSupportForm}
>
{isSubmitting ? 'Submitting...' : 'Submit Feedback'}
Open Feedback Form
</Button>
</YStack>
</View>
@@ -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,
},
});

View File

@@ -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';

View File

@@ -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<ReturnType<typeof setTimeout> | null>(null);
const [isVisible, setIsVisible] = useState(false);
const [isModalVisible, setIsModalVisible] = useState(false);
const [modalParams, setModalParams] =
useState<FeedbackModalScreenParams | null>(null);
const [modalParams, setModalParams] = useState<AlertModalParams | null>(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);
}, []);

View File

@@ -35,20 +35,26 @@ export default function SimpleScrolledTitleLayout({
footer,
}: DetailListProps) {
const insets = useSafeAreaInsets();
const dismissBottomPadding = Math.min(16, insets.bottom);
return (
<ExpandableBottomLayout.Layout backgroundColor={white}>
<ExpandableBottomLayout.FullSection paddingTop={0} flex={1}>
<YStack paddingTop={insets.top + 12}>
<YStack paddingTop={insets.top + 24}>
<Title>{title}</Title>
{header}
</YStack>
<ScrollView flex={1} showsVerticalScrollIndicator={false}>
<ScrollView
flex={1}
showsVerticalScrollIndicator={true}
indicatorStyle="black"
scrollIndicatorInsets={{ right: 1 }}
>
<YStack paddingTop={0} paddingBottom={12} flex={1}>
{children}
</YStack>
</ScrollView>
{footer && (
<YStack marginTop={8} marginBottom={12}>
<YStack marginTop={16} marginBottom={12}>
{footer}
</YStack>
)}
@@ -60,8 +66,8 @@ export default function SimpleScrolledTitleLayout({
{secondaryButtonText}
</SecondaryButton>
)}
{/* Anchor the Dismiss button to bottom with only safe area padding */}
<YStack paddingBottom={insets.bottom + 8}>
{/* Anchor the Dismiss button to bottom with sane spacing */}
<YStack marginTop="auto" paddingBottom={dismissBottomPadding}>
<PrimaryButton onPress={onDismiss}>Dismiss</PrimaryButton>
</YStack>
</ExpandableBottomLayout.FullSection>

View File

@@ -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<void>;
showModal: (params: FeedbackModalScreenParams) => void;
showModal: (params: AlertModalParams) => void;
}
const FeedbackContext = createContext<FeedbackContextType | undefined>(
@@ -50,13 +50,9 @@ export const FeedbackProvider: React.FC<FeedbackProviderProps> = ({
>
{children}
<FeedbackModal
visible={isVisible}
onClose={hideFeedbackModal}
onSubmit={submitFeedback}
/>
<FeedbackModal visible={isVisible} onClose={hideFeedbackModal} />
<FeedbackModalScreen
<AlertModal
visible={isModalVisible}
modalParams={modalParams}
onHideModal={hideModal}

View File

@@ -6,7 +6,6 @@ import type { PropsWithChildren } from 'react';
import React, { useCallback, useMemo, useState } from 'react';
import { Linking, Platform, Share, View as RNView } from 'react-native';
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
import { getCountry, getLocales, getTimeZone } from 'react-native-localize';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import type { SvgProps } from 'react-native-svg';
import { Button, ScrollView, View, XStack, YStack } from 'tamagui';
@@ -45,10 +44,10 @@ import {
} from '@/consts/links';
import { impactLight } from '@/integrations/haptics';
import { usePassport } from '@/providers/passportDataProvider';
import { openSupportForm } from '@/services/support';
import { useSettingStore } from '@/stores/settingStore';
import { extraYPadding } from '@/utils/styleUtils';
import { version } from '../../../../package.json';
// Avoid importing RootStackParamList to prevent type cycles; use minimal typing
type MinimalRootStackParamList = Record<string, object | undefined>;
@@ -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<SvgProps>, 'Proof settings', 'ProofSettings'],
[Feedback, 'Send feedback', 'email_feedback'],
[Feedback, 'Get support', 'support_form'],
[ShareIcon, 'Share Self app', 'share'],
[
FileText as React.FC<SvgProps>,
@@ -90,7 +88,7 @@ const routes =
: ([
[Data, 'View document info', 'DocumentDataInfo'],
[Settings2 as React.FC<SvgProps>, 'Proof settings', 'ProofSettings'],
[Feedback, 'Send feeback', 'email_feedback'],
[Feedback, 'Get support', 'support_form'],
[
FileText as React.FC<SvgProps>,
'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':

View File

@@ -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 = () => {
</BodyText>
</>
)}
<BodyText style={[styles.disclaimer, { marginTop: 12 }]}>
{SUPPORT_FORM_MESSAGE}
</BodyText>
</TextsContainer>
<ButtonsContainer>
<PrimaryButton
@@ -634,7 +634,7 @@ const DocumentNFCScanScreen: React.FC = () => {
Cancel
</SecondaryButton>
<SecondaryButton onPress={onReportIssue}>
Report Issue
{SUPPORT_FORM_BUTTON_TEXT}
</SecondaryButton>
</ButtonsContainer>
</>

View File

@@ -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
<YStack marginTop={16} marginBottom={0} gap={10}>
<SecondaryButton
onPress={() =>
sendFeedbackEmail({
message: 'User reported an issue from NFC trouble screen',
origin: 'passport/nfc-trouble',
})
}
style={{ marginBottom: 0 }}
>
Report Issue
</SecondaryButton>
</YStack>
<SecondaryButton onPress={openSupportForm} style={{ marginBottom: 0 }}>
{SUPPORT_FORM_BUTTON_TEXT}
</SecondaryButton>
}
>
<YStack

View File

@@ -26,7 +26,11 @@ import { notificationError } from '@/integrations/haptics';
import { ExpandableBottomLayout } from '@/layouts/ExpandableBottomLayout';
import type { SharedRoutesParamList } from '@/navigation/types';
import { flush as flushAnalytics } from '@/services/analytics';
import { sendCountrySupportNotification } from '@/services/email';
import {
openSupportForm,
SUPPORT_FORM_COMING_SOON_BUTTON_TEXT,
SUPPORT_FORM_COMING_SOON_MESSAGE,
} from '@/services/support';
type ComingSoonScreenProps = NativeStackScreenProps<
SharedRoutesParamList,
@@ -83,13 +87,9 @@ const ComingSoonScreen: React.FC<ComingSoonScreenProps> = ({ 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<ComingSoonScreenProps> = ({ route }) => {
return (
<ExpandableBottomLayout.Layout backgroundColor={black}>
<ExpandableBottomLayout.TopSection backgroundColor={white}>
<YStack
flex={1}
justifyContent="center"
alignItems="center"
marginTop={100}
>
<ExpandableBottomLayout.TopSection
backgroundColor={white}
overflow="visible"
>
<YStack flex={1} justifyContent="center" alignItems="center">
<XStack
justifyContent="center"
alignItems="center"
@@ -124,6 +122,7 @@ const ComingSoonScreen: React.FC<ComingSoonScreenProps> = ({ route }) => {
textAlign: 'center',
color: black,
marginBottom: 16,
paddingTop: 10,
}}
>
Coming Soon
@@ -150,7 +149,7 @@ const ComingSoonScreen: React.FC<ComingSoonScreenProps> = ({ route }) => {
paddingHorizontal: 10,
}}
>
Sign up for live updates.
{SUPPORT_FORM_COMING_SOON_MESSAGE}
</BodyText>
</YStack>
</ExpandableBottomLayout.TopSection>
@@ -158,13 +157,14 @@ const ComingSoonScreen: React.FC<ComingSoonScreenProps> = ({ route }) => {
gap={16}
backgroundColor={white}
paddingHorizontal={20}
paddingVertical={20}
paddingTop={20}
paddingBottom={20}
>
<PrimaryButton
onPress={onNotifyMe}
trackEvent={PassportEvents.NOTIFY_COMING_SOON}
>
Sign up for updates
{SUPPORT_FORM_COMING_SOON_BUTTON_TEXT}
</PrimaryButton>
<SecondaryButton
trackEvent={PassportEvents.DISMISS_COMING_SOON}

View File

@@ -71,7 +71,7 @@ const StarfallPushCodeScreen: React.FC = () => {
};
const handleCopyCode = async () => {
if (!code || code === DASH_CODE) {
if (isLoading || isCopied || !code || code === DASH_CODE) {
return;
}
@@ -225,14 +225,18 @@ const StarfallPushCodeScreen: React.FC = () => {
<PrimaryButton
onPress={handleCopyCode}
disabled={isCopied || !code || code === DASH_CODE || isLoading}
fontSize={16}
accessibilityState={{
disabled: isCopied || !code || code === DASH_CODE || isLoading,
}}
style={{
backgroundColor: isCopied ? green500 : undefined,
borderColor: '#374151',
borderWidth: 1,
borderRadius: 60,
height: 46,
opacity:
isCopied || !code || code === DASH_CODE || isLoading ? 0.6 : 1,
paddingVertical: 0,
}}
>

View File

@@ -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 = () => {
<SimpleScrolledTitleLayout
title="Having trouble scanning the QR code?"
onDismiss={go}
footer={
<SecondaryButton onPress={openSupportForm}>
{SUPPORT_FORM_BUTTON_TEXT}
</SecondaryButton>
}
>
<Caption size="large" style={{ color: slate500 }}>
<Caption size="large" style={{ color: slate500, marginBottom: 16 }}>
Here are some tips to help you successfully scan the QR code:
</Caption>
<Tips items={tips} />
<Tips items={tipsDeeplink} />
<View marginBottom={24}>
<Tips items={tips} />
<Tips items={tipsDeeplink} />
</View>
</SimpleScrolledTitleLayout>
);
};

View File

@@ -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<void> => {
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<void> => {
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)}`,
);
};

View File

@@ -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<void> => {
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.',
);
}
};