Skip to content

Commit

Permalink
feat: setup generic permission requesting logic (#395)
Browse files Browse the repository at this point in the history
  • Loading branch information
tsyirvo authored Nov 1, 2024
1 parent 5d95523 commit 4f4450d
Show file tree
Hide file tree
Showing 21 changed files with 295 additions and 64 deletions.
14 changes: 10 additions & 4 deletions app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,12 +63,11 @@ const plugins: ExpoConfig['plugins'] = [
'expo-secure-store',
[
'onesignal-expo-plugin',
{
mode: isDevelopmentEnv ? 'development' : 'production',
},
{ mode: isDevelopmentEnv ? 'development' : 'production' },
],
'expo-router',
['react-native-appsflyer', {}],
['react-native-permissions', { iosPermissions: ['Notifications'] }],
];

// eslint-disable-next-line import/no-default-export
Expand All @@ -80,7 +79,7 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
scheme: 'rn-starter',
slug: 'rn-starter',
version: Env.VERSION.toString(),
runtimeVersion: { policy: 'fingerprint' },
runtimeVersion: { policy: 'appVersion' },
jsEngine: 'hermes',
orientation: 'portrait',
icon: './assets/icon.png',
Expand All @@ -104,6 +103,9 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
config: {
usesNonExemptEncryption: false,
},
infoPlist: {
CFBundleAllowMixedLocalizations: true,
},
},
android: {
adaptiveIcon: {
Expand Down Expand Up @@ -136,6 +138,10 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
},
],
},
locales: {
fr: './src/core/i18n/infoPlist/fr.json',
en: './src/core/i18n/infoPlist/en.json',
},
experiments: {
typedRoutes: true,
},
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
"@react-navigation/native": "6.1.17",
"@react-navigation/native-stack": "6.9.26",
"@sentry/react-native": "~5.24.3",
"@shopify/flash-list": "1.6.4",
"@shopify/restyle": "2.4.4",
"@tanstack/query-async-storage-persister": "5.59.13",
"@tanstack/react-query": "5.59.13",
Expand Down Expand Up @@ -113,6 +114,7 @@
"react-native-keyboard-controller": "1.14.1",
"react-native-mmkv": "2.12.2",
"react-native-onesignal": "5.1.1",
"react-native-permissions": "5.1.0",
"react-native-purchases": "8.2.4",
"react-native-reanimated": "~3.15.0",
"react-native-safe-area-context": "4.10.5",
Expand Down
2 changes: 1 addition & 1 deletion src/app/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { colors } from '$core/theme';
import { toastConfig } from '$core/toaster';
import { AppUpdateNeeded } from '$shared/components/AppUpdateNeeded';
import { MaintenanceMode } from '$shared/components/MaintenanceMode';
import { useCheckNetworkStateOnMount } from '$shared/hooks/useCheckNetworkStateOnMount';
import { useCheckNetworkStateOnMount } from '$shared/hooks';
import 'react-native-gesture-handler';

import '../core/i18n';
Expand Down
1 change: 1 addition & 0 deletions src/core/i18n/infoPlist/en.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
1 change: 1 addition & 0 deletions src/core/i18n/infoPlist/fr.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
4 changes: 4 additions & 0 deletions src/core/i18n/resources/en/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
"failure": "The language could not be changed",
"success": "The language has been changed"
},
"permissions": {
"notAvailable": "This permission is not available on this device",
"notGranted": "You rejected this permission request"
},
"updateAvailable": {
"banner": {
"compareVersions": "Version {{storeVersion}} of the app is now available. You are currently on version {{currentVersion}}.",
Expand Down
16 changes: 16 additions & 0 deletions src/core/i18n/resources/fr/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,21 @@
"changeLocale": {
"failure": "La langue n'a pas pu être changée",
"success": "La langue a bien été changée"
},
"permissions": {
"notAvailable": "Cette permission n'est pas disponible sur cet appareil",
"notGranted": "Vous avez refusé cette demande de permission"
},
"updateAvailable": {
"banner": {
"compareVersions": "La version {{storeVersion}} de l'app est maintenant disponible. Vous êtes actuellement sur la version {{currentVersion}}.",
"defaultTitle": "Une nouvelle version de l'app est disponible.",
"updateCta": "Mettre à jour"
},
"nativePrompt": {
"message": "Une nouvelle version est disponible. Voulez-vous mettre à jour maintenant?",
"title": "Mise à jour disponible",
"updateCta": "Mettre à jour"
}
}
}
2 changes: 1 addition & 1 deletion src/core/navigation/hooks/useAppStateTracking.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Analytics } from '$core/analytics';
import { useAppState } from '$shared/hooks/useAppState';
import { useAppState } from '$shared/hooks';

export const useAppStateTracking = () => {
useAppState({
Expand Down
23 changes: 0 additions & 23 deletions src/core/notifications/notifications.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,29 +52,6 @@ class NotificationsClass {
OneSignal.User.pushSubscription.optIn();
}

/* ***** ***** Permission ***** ***** */

async checkPermissions() {
const isPermissionGranted =
await OneSignal.Notifications.getPermissionAsync();

return isPermissionGranted;
}

async canRequestPermission() {
const isRequestPossible =
await OneSignal.Notifications.canRequestPermission();

return isRequestPossible;
}

async requestPermissions() {
const isPermissionGranted =
await OneSignal.Notifications.requestPermission(false);

return isPermissionGranted;
}

/* ***** ***** Listeners ***** ***** */

watchForNotificationPress() {
Expand Down
1 change: 1 addition & 0 deletions src/core/permissions/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { Permissions } from './permissions';
64 changes: 64 additions & 0 deletions src/core/permissions/permissions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import type { Permission, PermissionStatus } from 'react-native-permissions';
import {
RESULTS,
check,
checkNotifications,
openSettings,
requestNotifications,
request as requestPermission,
} from 'react-native-permissions';

class PermissionsClass {
/* ***** ***** Utils ***** ***** */

decodeStatus(status: PermissionStatus) {
switch (status) {
case RESULTS.UNAVAILABLE:
return { isAvailable: false, isGranted: false, isRequestable: false };
case RESULTS.DENIED:
return { isAvailable: true, isGranted: false, isRequestable: true };
case RESULTS.BLOCKED:
// Is never returned on Android. A call to request will return it
return { isAvailable: true, isGranted: false, isRequestable: false };
case RESULTS.GRANTED:
case RESULTS.LIMITED:
return { isAvailable: true, isGranted: true, isRequestable: false };
default:
return { isAvailable: false, isGranted: false, isRequestable: false };
}
}

async openSystemSettings(type?: 'application' | 'alarms' | 'notifications') {
await openSettings(type);
}

/* ***** ***** Permissions ***** ***** */

async checkStatus(permission: Permission) {
const status = await check(permission);

return this.decodeStatus(status);
}

async request(permission: Permission) {
const requestStatus = await requestPermission(permission);

return this.decodeStatus(requestStatus);
}

/* ***** ***** Notifications ***** ***** */

async checkNotificationsStatus() {
const { status } = await checkNotifications();

return this.decodeStatus(status);
}

async requestNotifications() {
const { status } = await requestNotifications(['alert', 'badge', 'sound']);

return this.decodeStatus(status);
}
}

export const Permissions = new PermissionsClass();
4 changes: 4 additions & 0 deletions src/core/testing/setup.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
// @ts-expect-error: doesn't resolve types
import mockReactNativePermissions from 'react-native-permissions/mock';
import mockSafeAreaContext from 'react-native-safe-area-context/jest/mock';

// eslint-disable-next-line @typescript-eslint/no-unsafe-return
jest.mock('react-native-safe-area-context', () => mockSafeAreaContext);
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
jest.mock('react-native-permissions', () => mockReactNativePermissions);
39 changes: 11 additions & 28 deletions src/features/notifications/Notifications.tsx
Original file line number Diff line number Diff line change
@@ -1,51 +1,34 @@
import * as Linking from 'expo-linking';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { Alert } from 'react-native';

import { Logger } from '$core/logger';
import { Notifications as NotificationsHandler } from '$core/notifications';
import { useRequestPermission } from '$shared/hooks';
import { Button } from '$shared/uiKit/button';
import { Box, Text } from '$shared/uiKit/primitives';

export const Notifications = () => {
const { t } = useTranslation('miscScreens');

const requestNotificationPermission = async () => {
const canRequestPermission =
await NotificationsHandler.canRequestPermission();
const isPermissionAlreadyGranted =
await NotificationsHandler.checkPermissions();
const { requestNotificationPermission } = useRequestPermission();

if (!canRequestPermission && !isPermissionAlreadyGranted) {
Linking.openSettings().catch((error: unknown) => {
Logger.error({
error,
level: 'warning',
message: 'Failed to open settings',
});
const onPress = async () => {
try {
await requestNotificationPermission();
} catch (error) {
Logger.error({
error,
message: 'Failed to request notification permission',
level: 'warning',
});

return;
}

if (isPermissionAlreadyGranted) {
Alert.alert('Permission already granted');

return;
}

await NotificationsHandler.requestPermissions();
};

return (
<>
<Text variant="large">{t('notifications.title')}</Text>

<Box alignItems="flex-start" mt="spacing_8">
<Button.Text onPress={requestNotificationPermission}>
{t('notifications.cta')}
</Button.Text>
<Button.Text onPress={onPress}>{t('notifications.cta')}</Button.Text>
</Box>
</>
);
Expand Down
2 changes: 1 addition & 1 deletion src/features/sandbox/Sandbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { ReactElement } from 'react';
import { Suspense, useState } from 'react';

import { Logger } from '$core/logger';
import { useRunOnMount } from '$shared/hooks/useRunOnMount';
import { useRunOnMount } from '$shared/hooks';
import { Loader } from '$shared/uiKit/Loader';

import { SuspendedDebugStack } from './navigation';
Expand Down
2 changes: 1 addition & 1 deletion src/shared/components/AppUpdateNeeded.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import semverGte from 'semver/functions/gte';
import { config, IS_IOS } from '$core/constants';
import { useGetFlagValueSync } from '$core/featureFlags/hooks/useGetFlagValueSync';
import { Logger } from '$core/logger';
import { useRunOnMount } from '$shared/hooks/useRunOnMount';
import { useRunOnMount } from '$shared/hooks';
import { Button } from '$shared/uiKit/button';
import { Box, Text } from '$shared/uiKit/primitives';

Expand Down
2 changes: 1 addition & 1 deletion src/shared/components/StoreUpdateBanner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';

import { Logger } from '$core/logger';
import { useRunOnMount } from '$shared/hooks/useRunOnMount';
import { useRunOnMount } from '$shared/hooks';
import { Button } from '$shared/uiKit/button';
import { Box, Text } from '$shared/uiKit/primitives';
import { checkForNativeUpdate } from '$shared/utils/checkForAppUpdates';
Expand Down
8 changes: 8 additions & 0 deletions src/shared/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export { useAppState } from './useAppState';
export { useCheckNetworkStateOnMount } from './useCheckNetworkStateOnMount';
export { useKeyboard } from './useKeyboard';
export { usePress } from './usePress';
export { usePreviousState } from './usePreviousState';
export { useRequestPermission } from './useRequestPermission';
export { useRunOnMount } from './useRunOnMount';
export { useWhyDidYouUpdate } from './useWhyDidYouUpdate';
Loading

0 comments on commit 4f4450d

Please sign in to comment.