mirror of
https://github.com/selfxyz/self.git
synced 2026-02-19 02:24:25 -05:00
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:
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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);
|
||||
}, []);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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':
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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,
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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)}`,
|
||||
);
|
||||
};
|
||||
42
app/src/services/support.ts
Normal file
42
app/src/services/support.ts
Normal 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.',
|
||||
);
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user