BlueWallet/screen/settings/NotificationSettings.tsx
Overtorment 0181f0a849
Some checks are pending
Build Release and Upload to TestFlight (iOS) / build (push) Waiting to run
Build Release and Upload to TestFlight (iOS) / testflight-upload (push) Blocked by required conditions
BuildReleaseApk / buildReleaseApk (push) Waiting to run
BuildReleaseApk / browserstack (push) Blocked by required conditions
ADD: arkade ln pushes (#8634)
2026-06-10 17:35:17 +01:00

342 lines
10 KiB
TypeScript

import React, { useCallback, useEffect, useState } from 'react';
import { StyleSheet, View, Pressable, AppState, Text } from 'react-native';
import {
getPushToken,
getStoredNotifications,
isNotificationsEnabled,
setLevels,
tryToObtainPermissions,
cleanUserOptOutFlag,
checkPermissions,
checkNotificationPermissionStatus,
enqueueTestPushNotification,
NOTIFICATIONS_NO_AND_DONT_ASK_FLAG,
} from '../../blue_modules/notifications';
import presentAlert from '../../components/Alert';
import { BlueSpacing20 } from '../../components/BlueSpacing';
import { Button } from '../../components/Button';
import CopyToClipboardButton from '../../components/CopyToClipboardButton';
import { useTheme } from '../../components/themes';
import loc from '../../loc';
import { openSettings } from 'react-native-permissions';
import AsyncStorage from '@react-native-async-storage/async-storage';
import {
SettingsCard,
SettingsFlatList,
SettingsListItem,
SettingsListItemProps,
SettingsSubtitle,
isAndroid,
} from '../../components/platform';
interface SettingItem extends SettingsListItemProps {
id: string;
section?: number;
customContent?: React.ReactNode;
}
const NotificationSettings: React.FC = () => {
const [isLoading, setIsLoading] = useState(true);
const [isNotificationsEnabledState, setNotificationsEnabledState] = useState<boolean | undefined>(undefined);
const [tokenInfo, setTokenInfo] = useState('<empty>');
const [tapCount, setTapCount] = useState(0);
const { colors } = useTheme();
const handleTap = () => {
setTapCount(prevCount => prevCount + 1);
};
const onSystemSettings = useCallback(() => {
openSettings('notifications');
}, []);
const showNotificationPermissionAlert = useCallback(() => {
presentAlert({
title: loc.settings.notifications,
message: loc.notifications.permission_denied_message,
buttons: [
{
text: loc._.ok,
style: 'cancel',
},
{
text: loc.settings.header,
onPress: onSystemSettings,
style: 'default',
},
],
});
}, [onSystemSettings]);
const onNotificationsSwitch = useCallback(
async (value: boolean) => {
if (value) {
const currentStatus = await checkNotificationPermissionStatus();
if (currentStatus === 'blocked') {
// If permissions are denied/blocked, show alert and reset the toggle
showNotificationPermissionAlert();
setNotificationsEnabledState(false);
return;
}
}
try {
setNotificationsEnabledState(value);
if (value) {
await cleanUserOptOutFlag();
const permissionsGranted = await tryToObtainPermissions();
if (permissionsGranted) {
await setLevels(true);
await AsyncStorage.removeItem(NOTIFICATIONS_NO_AND_DONT_ASK_FLAG);
} else {
showNotificationPermissionAlert();
setNotificationsEnabledState(false);
}
} else {
await setLevels(false);
await AsyncStorage.setItem(NOTIFICATIONS_NO_AND_DONT_ASK_FLAG, 'true');
setNotificationsEnabledState(false);
}
} catch (error) {
console.error(error);
presentAlert({ message: (error as Error).message });
setNotificationsEnabledState(false);
}
},
[showNotificationPermissionAlert, setNotificationsEnabledState],
);
const updateNotificationStatus = async () => {
try {
const currentStatus = await checkNotificationPermissionStatus();
const isEnabled = await isNotificationsEnabled();
const isDisabledByUser = (await AsyncStorage.getItem(NOTIFICATIONS_NO_AND_DONT_ASK_FLAG)) === 'true';
if (!isDisabledByUser) {
setNotificationsEnabledState(currentStatus === 'granted' && isEnabled);
} else {
setNotificationsEnabledState(false);
}
} catch (error) {
console.log('Error updating notification status:', error);
}
};
useEffect(() => {
(async () => {
try {
const isDisabledByUser = (await AsyncStorage.getItem(NOTIFICATIONS_NO_AND_DONT_ASK_FLAG)) === 'true';
if (isDisabledByUser) {
console.debug('Notifications were disabled by the user. Skipping auto-activation.');
setNotificationsEnabledState(false);
} else {
await updateNotificationStatus();
}
setTokenInfo(
'token: ' +
JSON.stringify(await getPushToken()) +
' permissions: ' +
JSON.stringify(await checkPermissions()) +
' stored notifications: ' +
JSON.stringify(await getStoredNotifications()),
);
} catch (e) {
console.error(e);
presentAlert({ message: (e as Error).message });
} finally {
setIsLoading(false);
}
})();
const appStateListener = AppState.addEventListener('change', nextAppState => {
if (nextAppState === 'active') {
setTimeout(async () => {
const isDisabledByUser = (await AsyncStorage.getItem(NOTIFICATIONS_NO_AND_DONT_ASK_FLAG)) === 'true';
if (!isDisabledByUser) {
updateNotificationStatus();
}
}, 300);
}
});
return () => {
appStateListener.remove();
};
}, []);
const enqueueTestPush = useCallback(async () => {
setIsLoading(true);
try {
await enqueueTestPushNotification();
} catch (error) {
console.error('Error enqueueing test push:', error);
presentAlert({ message: (error as Error).message });
} finally {
setIsLoading(false);
}
}, []);
const renderDeveloperSettings = useCallback(() => {
if (tapCount < 10) return null;
return (
<View>
<View style={[styles.divider, { backgroundColor: colors.lightBorder ?? colors.borderTopColor }]} />
<SettingsCard style={styles.card}>
<View style={styles.cardContent}>
<Text style={[styles.centered, { color: colors.foregroundColor }]} onPress={() => setTapCount(tapCount + 1)}>
Ground Control to Major Tom
</Text>
<Text style={[styles.centered, { color: colors.foregroundColor }]} onPress={() => setTapCount(tapCount + 1)}>
Commencing countdown, engines on
</Text>
<View>
<CopyToClipboardButton stringToCopy={tokenInfo} displayText={tokenInfo} />
</View>
<BlueSpacing20 />
<Button onPress={enqueueTestPush} title="Enqueue test push notification" disabled={isLoading} />
</View>
</SettingsCard>
</View>
);
}, [tapCount, colors, isLoading, tokenInfo, enqueueTestPush]);
const renderPushNotificationsExplanation = useCallback(() => {
return (
<SettingsCard compact style={styles.notificationsExplanationCard}>
<View style={styles.cardContent}>
<Pressable onPress={handleTap}>
<SettingsSubtitle>{loc.settings.push_notifications_explanation}</SettingsSubtitle>
</Pressable>
</View>
</SettingsCard>
);
}, []);
const settingsItems = useCallback((): SettingItem[] => {
const items: SettingItem[] = [
{
id: 'notificationsToggle',
title: loc.settings.notifications,
subtitle: loc.notifications.notifications_subtitle,
switch: {
value: isNotificationsEnabledState || false,
onValueChange: onNotificationsSwitch,
disabled: isLoading,
},
isLoading: isNotificationsEnabledState === undefined,
testID: 'NotificationsSwitch',
Component: View,
section: 1,
},
{
id: 'notificationsExplanation',
title: '',
customContent: renderPushNotificationsExplanation(),
section: 1,
},
{
id: 'section1Spacing',
title: '',
customContent: <View style={styles.sectionSpacing} />,
section: 1.5,
},
{
id: 'developerSettings',
title: '',
customContent: renderDeveloperSettings(),
section: 2,
},
{
id: 'section2Spacing',
title: '',
customContent: <View style={styles.sectionSpacing} />,
section: 2.5,
},
{
id: 'privacySystemSettings',
title: loc.settings.privacy_system_settings,
onPress: onSystemSettings,
section: 3,
},
];
return items.filter(item => item.title !== '' || item.customContent);
}, [
isNotificationsEnabledState,
onNotificationsSwitch,
isLoading,
renderDeveloperSettings,
renderPushNotificationsExplanation,
onSystemSettings,
]);
const renderItem = useCallback(
(props: { item: SettingItem }) => {
const { id, section, customContent, ...listItemProps } = props.item;
const items = settingsItems();
const contentPadding = !isAndroid ? { paddingHorizontal: horizontalPadding } : undefined;
if (customContent) {
return <View style={contentPadding}>{customContent}</View>;
}
const sectionItems = items.filter(i => i.section === section && !i.customContent);
const indexInSection = sectionItems.findIndex(i => i.id === id);
const isFirstInSection = indexInSection === 0;
const isLastInSection = indexInSection === sectionItems.length - 1;
const position = isFirstInSection && isLastInSection ? 'single' : isFirstInSection ? 'first' : isLastInSection ? 'last' : 'middle';
return <SettingsListItem {...listItemProps} position={position} />;
},
[settingsItems],
);
const keyExtractor = useCallback((item: SettingItem) => item.id, []);
return (
<SettingsFlatList
data={settingsItems()}
renderItem={renderItem}
keyExtractor={keyExtractor}
contentInsetAdjustmentBehavior="automatic"
automaticallyAdjustContentInsets
removeClippedSubviews
/>
);
};
export default NotificationSettings;
const horizontalPadding = isAndroid ? 20 : 16;
const styles = StyleSheet.create({
card: {
marginVertical: isAndroid ? 8 : 0,
},
notificationsExplanationCard: {
marginVertical: isAndroid ? 12 : 10,
},
cardContent: {
paddingHorizontal: horizontalPadding,
paddingVertical: isAndroid ? 12 : 10,
},
centered: {
textAlign: 'center',
marginVertical: 4,
},
divider: {
marginVertical: isAndroid ? 16 : 12,
height: 0.5,
},
sectionSpacing: {
height: isAndroid ? 24 : 12,
},
});