@wasto/cms-components 1.5.11 → 1.5.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +79 -50
- package/dist/index.d.ts +79 -50
- package/dist/index.js +1054 -874
- package/dist/index.mjs +390 -211
- package/package.json +11 -1
- package/src/components/header/Header.tsx +33 -9
- package/src/components/locations-map/LocationsMapShell.tsx +24 -5
- package/src/components/push/PushTokenRegistrar.tsx +47 -0
- package/src/contexts/HeaderConfigContext.tsx +29 -0
- package/src/contexts/WastoContext.tsx +2 -0
- package/src/index.ts +12 -0
- package/src/push/registerExpoPushToken.ts +133 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wasto/cms-components",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.12",
|
|
4
4
|
"description": "Wast'O CMS React Native components",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"module": "./src/index.ts",
|
|
@@ -30,10 +30,12 @@
|
|
|
30
30
|
"@react-navigation/bottom-tabs": "*",
|
|
31
31
|
"@sentry/react-native": "*",
|
|
32
32
|
"@wasto/sdk": "*",
|
|
33
|
+
"expo-constants": "*",
|
|
33
34
|
"expo-haptics": "*",
|
|
34
35
|
"expo-image": "*",
|
|
35
36
|
"expo-image-picker": "*",
|
|
36
37
|
"expo-linear-gradient": "*",
|
|
38
|
+
"expo-notifications": "*",
|
|
37
39
|
"expo-router": "*",
|
|
38
40
|
"lucide-react-native": "*",
|
|
39
41
|
"maplibre-gl": "*",
|
|
@@ -51,12 +53,18 @@
|
|
|
51
53
|
"@sentry/react-native": {
|
|
52
54
|
"optional": true
|
|
53
55
|
},
|
|
56
|
+
"expo-constants": {
|
|
57
|
+
"optional": true
|
|
58
|
+
},
|
|
54
59
|
"expo-image-picker": {
|
|
55
60
|
"optional": true
|
|
56
61
|
},
|
|
57
62
|
"expo-linear-gradient": {
|
|
58
63
|
"optional": true
|
|
59
64
|
},
|
|
65
|
+
"expo-notifications": {
|
|
66
|
+
"optional": true
|
|
67
|
+
},
|
|
60
68
|
"expo-router": {
|
|
61
69
|
"optional": true
|
|
62
70
|
},
|
|
@@ -69,8 +77,10 @@
|
|
|
69
77
|
"@react-navigation/bottom-tabs": "^7.4.0",
|
|
70
78
|
"@sentry/react-native": "~6.22.0",
|
|
71
79
|
"@types/react": "~19.2.2",
|
|
80
|
+
"expo-constants": "~18.0.9",
|
|
72
81
|
"expo-haptics": "~15.0.7",
|
|
73
82
|
"expo-linear-gradient": "~15.0.8",
|
|
83
|
+
"expo-notifications": "~0.32.17",
|
|
74
84
|
"maplibre-gl": "^4.7.0",
|
|
75
85
|
"react-native-gesture-handler": "~2.28.0",
|
|
76
86
|
"react-native-markdown-display": "^7.0.2",
|
|
@@ -58,6 +58,26 @@ export type AppHeaderConfig = {
|
|
|
58
58
|
elements: AppHeaderElement[];
|
|
59
59
|
};
|
|
60
60
|
|
|
61
|
+
/** Actions standard (notifications + profil) pour navbar sans titre. */
|
|
62
|
+
export const DEFAULT_HEADER_ACTIONS: readonly AppHeaderActionElement[] = [
|
|
63
|
+
{
|
|
64
|
+
id: "notifications",
|
|
65
|
+
type: "action",
|
|
66
|
+
slot: "right",
|
|
67
|
+
icon: "bell",
|
|
68
|
+
action: "navigate",
|
|
69
|
+
href: SYSTEM_ROUTES.notifications,
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
id: "profile",
|
|
73
|
+
type: "action",
|
|
74
|
+
slot: "right",
|
|
75
|
+
icon: "person",
|
|
76
|
+
action: "navigate",
|
|
77
|
+
href: SYSTEM_ROUTES.profile,
|
|
78
|
+
},
|
|
79
|
+
];
|
|
80
|
+
|
|
61
81
|
export type HeaderProps = {
|
|
62
82
|
readonly options: { title?: string; headerBackVisible?: boolean };
|
|
63
83
|
readonly route: { name: string };
|
|
@@ -67,7 +87,10 @@ export type HeaderProps = {
|
|
|
67
87
|
};
|
|
68
88
|
/** Flotte au-dessus du contenu (ex. carte) : fond transparent. */
|
|
69
89
|
readonly overlay?: boolean;
|
|
70
|
-
/**
|
|
90
|
+
/**
|
|
91
|
+
* Config layout (`header.elements`) — logo / actions.
|
|
92
|
+
* Si fournie (même vide), le fallback titre d’écran ne s’applique pas.
|
|
93
|
+
*/
|
|
71
94
|
readonly headerConfig?: AppHeaderConfig | null;
|
|
72
95
|
};
|
|
73
96
|
|
|
@@ -143,8 +166,13 @@ export function Header({
|
|
|
143
166
|
[options.title, route.name],
|
|
144
167
|
);
|
|
145
168
|
|
|
146
|
-
|
|
147
|
-
const
|
|
169
|
+
/** Config explicite → navbar (logo/actions), jamais le fallback titre d’écran. */
|
|
170
|
+
const hasExplicitConfig = headerConfig != null;
|
|
171
|
+
const elements = useMemo(() => {
|
|
172
|
+
const raw = headerConfig?.elements ?? [];
|
|
173
|
+
// `type: "title"` est déprécié (navbar uniquement).
|
|
174
|
+
return raw.filter((el) => el.type !== "title");
|
|
175
|
+
}, [headerConfig]);
|
|
148
176
|
const hasCustomActions = elements.some((el) => el.type === "action");
|
|
149
177
|
|
|
150
178
|
const bySlot = useMemo(() => {
|
|
@@ -174,11 +202,7 @@ export function Header({
|
|
|
174
202
|
return <HeaderLogo key={el.id} element={el} />;
|
|
175
203
|
}
|
|
176
204
|
if (el.type === "title") {
|
|
177
|
-
|
|
178
|
-
el.useScreenTitle !== false
|
|
179
|
-
? screenTitle
|
|
180
|
-
: (el.text || "").trim() || screenTitle;
|
|
181
|
-
return <Title key={el.id} text={text} level="1" />;
|
|
205
|
+
return null;
|
|
182
206
|
}
|
|
183
207
|
// action
|
|
184
208
|
const onPress = async () => {
|
|
@@ -259,7 +283,7 @@ export function Header({
|
|
|
259
283
|
pointerEvents="box-none"
|
|
260
284
|
>
|
|
261
285
|
<View style={styles.header} pointerEvents="box-none">
|
|
262
|
-
{
|
|
286
|
+
{hasExplicitConfig ? (
|
|
263
287
|
<>
|
|
264
288
|
<View style={styles.slotLeft} pointerEvents="box-none">
|
|
265
289
|
{showBack ? (
|
|
@@ -27,15 +27,22 @@ import { LocationSheet } from "./LocationSheet";
|
|
|
27
27
|
import type { LocationsMapViewProps } from "./LocationsMapView";
|
|
28
28
|
import { cardStyle, resolveTheme } from "../../helpers/themeStyles";
|
|
29
29
|
import { getContrastColor } from "../../helpers/getContrastColor";
|
|
30
|
-
import { Header } from "../header/Header";
|
|
30
|
+
import { Header, DEFAULT_HEADER_ACTIONS, type AppHeaderConfig } from "../header/Header";
|
|
31
|
+
import { useHeaderConfig } from "../../contexts/HeaderConfigContext";
|
|
31
32
|
import { SPACE } from "../../helpers/layout";
|
|
32
33
|
|
|
33
34
|
export type LocationsMapProps = {
|
|
34
35
|
fullScreen?: boolean;
|
|
35
36
|
showFilters?: boolean;
|
|
36
|
-
/** Header flottant au-dessus de la carte (
|
|
37
|
+
/** Header flottant au-dessus de la carte (même navbar que le layout, sans titre). */
|
|
37
38
|
overlayHeader?: boolean;
|
|
39
|
+
/**
|
|
40
|
+
* @deprecated Ignoré — la navbar n’affiche plus de titre d’écran.
|
|
41
|
+
* Conservé pour compatibilité des configs CMS existantes.
|
|
42
|
+
*/
|
|
38
43
|
overlayTitle?: string;
|
|
44
|
+
/** Override optionnel ; sinon config du layout via `HeaderConfigProvider`. */
|
|
45
|
+
headerConfig?: AppHeaderConfig | null;
|
|
39
46
|
};
|
|
40
47
|
|
|
41
48
|
const CATEGORIES = Object.keys(LOCATION_CATEGORY_META) as LocationCategory[];
|
|
@@ -168,17 +175,28 @@ export function LocationsMapShell({
|
|
|
168
175
|
fullScreen = false,
|
|
169
176
|
showFilters = true,
|
|
170
177
|
overlayHeader = false,
|
|
171
|
-
|
|
178
|
+
headerConfig: headerConfigProp,
|
|
172
179
|
MapCanvas,
|
|
173
180
|
}: LocationsMapShellProps): ReactNode {
|
|
174
181
|
const { theme } = useTheme();
|
|
175
182
|
const t = resolveTheme(theme);
|
|
176
183
|
const themedStyles = createThemedStyles(theme);
|
|
184
|
+
const layoutHeaderConfig = useHeaderConfig();
|
|
177
185
|
const { locations, isLoading, error, center } = useLocations();
|
|
178
186
|
const [filters, setFilters] = useState<Set<LocationCategory>>(new Set());
|
|
179
187
|
const [selected, setSelected] = useState<MapLocation | null>(null);
|
|
180
188
|
const [filtersOpen, setFiltersOpen] = useState(false);
|
|
181
189
|
|
|
190
|
+
const overlayHeaderConfig: AppHeaderConfig = useMemo(() => {
|
|
191
|
+
const raw = headerConfigProp ?? layoutHeaderConfig;
|
|
192
|
+
if (raw?.elements?.length) {
|
|
193
|
+
return {
|
|
194
|
+
elements: raw.elements.filter((el) => el.type !== "title"),
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
return { elements: DEFAULT_HEADER_ACTIONS.map((a) => ({ ...a })) };
|
|
198
|
+
}, [headerConfigProp, layoutHeaderConfig]);
|
|
199
|
+
|
|
182
200
|
const toggle = (category: LocationCategory) => {
|
|
183
201
|
setFilters((prev) => {
|
|
184
202
|
const next = new Set(prev);
|
|
@@ -252,9 +270,10 @@ export function LocationsMapShell({
|
|
|
252
270
|
{overlayHeader ? (
|
|
253
271
|
<Header
|
|
254
272
|
overlay
|
|
255
|
-
options={{
|
|
256
|
-
route={{ name:
|
|
273
|
+
options={{ headerBackVisible: false }}
|
|
274
|
+
route={{ name: "map" }}
|
|
257
275
|
navigation={NOOP_NAV}
|
|
276
|
+
headerConfig={overlayHeaderConfig}
|
|
258
277
|
/>
|
|
259
278
|
) : null}
|
|
260
279
|
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { useEffect, useRef, type ReactNode } from "react";
|
|
2
|
+
import { Platform } from "react-native";
|
|
3
|
+
import { useLocal } from "../../contexts/LocalContext";
|
|
4
|
+
import { useWasto } from "../../contexts/WastoContext";
|
|
5
|
+
import { useWastoClient } from "../../hooks/useWastoClient";
|
|
6
|
+
import { registerExpoPushToken } from "../../push/registerExpoPushToken";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Registers the Expo push token once a deviceId exists.
|
|
10
|
+
* Renders nothing — mount inside LocalProvider.
|
|
11
|
+
*/
|
|
12
|
+
export function PushTokenRegistrar(): ReactNode {
|
|
13
|
+
const { deviceId } = useLocal();
|
|
14
|
+
const { previewMode, reportError } = useWasto();
|
|
15
|
+
const client = useWastoClient();
|
|
16
|
+
const lastRegistered = useRef<string | null>(null);
|
|
17
|
+
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
if (previewMode || Platform.OS === "web" || !deviceId) return;
|
|
20
|
+
if (lastRegistered.current === deviceId) return;
|
|
21
|
+
|
|
22
|
+
let cancelled = false;
|
|
23
|
+
|
|
24
|
+
void (async () => {
|
|
25
|
+
try {
|
|
26
|
+
const token = await registerExpoPushToken({
|
|
27
|
+
deviceId,
|
|
28
|
+
registerPushToken: (id: string, pushToken: string) =>
|
|
29
|
+
client.public.devices.registerPushToken(id, { token: pushToken }),
|
|
30
|
+
});
|
|
31
|
+
if (!cancelled && token) {
|
|
32
|
+
lastRegistered.current = deviceId;
|
|
33
|
+
}
|
|
34
|
+
} catch (error) {
|
|
35
|
+
if (!cancelled) {
|
|
36
|
+
reportError(error, { source: "push", deviceId });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
})();
|
|
40
|
+
|
|
41
|
+
return () => {
|
|
42
|
+
cancelled = true;
|
|
43
|
+
};
|
|
44
|
+
}, [client.public.devices, deviceId, previewMode, reportError]);
|
|
45
|
+
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createContext,
|
|
3
|
+
useContext,
|
|
4
|
+
type ReactNode,
|
|
5
|
+
} from "react";
|
|
6
|
+
import type { AppHeaderConfig } from "../components/header/Header";
|
|
7
|
+
|
|
8
|
+
const HeaderConfigContext = createContext<AppHeaderConfig | null>(null);
|
|
9
|
+
|
|
10
|
+
export type HeaderConfigProviderProps = {
|
|
11
|
+
readonly value: AppHeaderConfig | null;
|
|
12
|
+
readonly children: ReactNode;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export function HeaderConfigProvider({
|
|
16
|
+
value,
|
|
17
|
+
children,
|
|
18
|
+
}: HeaderConfigProviderProps): ReactNode {
|
|
19
|
+
return (
|
|
20
|
+
<HeaderConfigContext.Provider value={value}>
|
|
21
|
+
{children}
|
|
22
|
+
</HeaderConfigContext.Provider>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Config header du layout parent (`null` si hors provider / non défini). */
|
|
27
|
+
export function useHeaderConfig(): AppHeaderConfig | null {
|
|
28
|
+
return useContext(HeaderConfigContext);
|
|
29
|
+
}
|
|
@@ -15,6 +15,7 @@ import { ChallengesProvider } from "./ChallengesContext";
|
|
|
15
15
|
import { AnnouncementsProvider } from "./AnnouncementsContext";
|
|
16
16
|
import SelectStreetModal from "../components/select-street-modal/SelectStreetModal";
|
|
17
17
|
import { AnnouncementModal } from "../components/announcement-modal/AnnouncementModal";
|
|
18
|
+
import { PushTokenRegistrar } from "../components/push/PushTokenRegistrar";
|
|
18
19
|
import { OptionalSafeAreaProvider } from "../components/safe-area/OptionalSafeAreaProvider";
|
|
19
20
|
import {
|
|
20
21
|
initWastoSentry,
|
|
@@ -104,6 +105,7 @@ export function WastoProvider(props: WastoProviderProps): ReactNode {
|
|
|
104
105
|
<WastoContext value={value}>
|
|
105
106
|
<OptionalSafeAreaProvider>
|
|
106
107
|
<LocalProvider previewMode={previewMode}>
|
|
108
|
+
<PushTokenRegistrar />
|
|
107
109
|
<ChallengesProvider>
|
|
108
110
|
<AnnouncementsProvider apiUrl={apiUrl}>
|
|
109
111
|
<ThemeProvider theme={props.theme}>
|
package/src/index.ts
CHANGED
|
@@ -63,6 +63,7 @@ export {
|
|
|
63
63
|
} from "./components/title/Title"
|
|
64
64
|
export {
|
|
65
65
|
Header,
|
|
66
|
+
DEFAULT_HEADER_ACTIONS,
|
|
66
67
|
type HeaderProps,
|
|
67
68
|
type AppHeaderConfig,
|
|
68
69
|
type AppHeaderElement,
|
|
@@ -98,6 +99,12 @@ export {
|
|
|
98
99
|
type WastoContextType,
|
|
99
100
|
} from "./contexts/WastoContext"
|
|
100
101
|
|
|
102
|
+
export {
|
|
103
|
+
HeaderConfigProvider,
|
|
104
|
+
useHeaderConfig,
|
|
105
|
+
type HeaderConfigProviderProps,
|
|
106
|
+
} from "./contexts/HeaderConfigContext"
|
|
107
|
+
|
|
101
108
|
export {
|
|
102
109
|
useAppData,
|
|
103
110
|
useOptionalAppData,
|
|
@@ -114,6 +121,11 @@ export {
|
|
|
114
121
|
type WastoErrorContext,
|
|
115
122
|
} from "./sentry/initSentry"
|
|
116
123
|
|
|
124
|
+
export {
|
|
125
|
+
registerExpoPushToken,
|
|
126
|
+
type RegisterExpoPushTokenOptions,
|
|
127
|
+
} from "./push/registerExpoPushToken"
|
|
128
|
+
|
|
117
129
|
export { useWastoClient } from "./hooks/useWastoClient"
|
|
118
130
|
|
|
119
131
|
export type {
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { Platform } from "react-native";
|
|
2
|
+
|
|
3
|
+
type NotificationsModule = typeof import("expo-notifications");
|
|
4
|
+
|
|
5
|
+
type ExpoConstantsLike = {
|
|
6
|
+
expoConfig?: {
|
|
7
|
+
extra?: {
|
|
8
|
+
eas?: {
|
|
9
|
+
projectId?: string;
|
|
10
|
+
};
|
|
11
|
+
};
|
|
12
|
+
} | null;
|
|
13
|
+
easConfig?: {
|
|
14
|
+
projectId?: string;
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
let notificationsModule: NotificationsModule | null | undefined;
|
|
19
|
+
let constantsModule: ExpoConstantsLike | null | undefined;
|
|
20
|
+
let handlerConfigured = false;
|
|
21
|
+
|
|
22
|
+
function loadNotifications(): NotificationsModule | null {
|
|
23
|
+
if (notificationsModule !== undefined) return notificationsModule;
|
|
24
|
+
try {
|
|
25
|
+
// Optional peer — apps without the package still load cms-components.
|
|
26
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
27
|
+
notificationsModule = require("expo-notifications") as NotificationsModule;
|
|
28
|
+
} catch {
|
|
29
|
+
notificationsModule = null;
|
|
30
|
+
}
|
|
31
|
+
return notificationsModule;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function loadConstants(): ExpoConstantsLike | null {
|
|
35
|
+
if (constantsModule !== undefined) return constantsModule;
|
|
36
|
+
try {
|
|
37
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
38
|
+
const mod = require("expo-constants") as
|
|
39
|
+
| ExpoConstantsLike
|
|
40
|
+
| { default: ExpoConstantsLike };
|
|
41
|
+
constantsModule = "default" in mod && mod.default ? mod.default : (mod as ExpoConstantsLike);
|
|
42
|
+
} catch {
|
|
43
|
+
constantsModule = null;
|
|
44
|
+
}
|
|
45
|
+
return constantsModule;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function resolveEasProjectId(): string | null {
|
|
49
|
+
const Constants = loadConstants();
|
|
50
|
+
const fromExtra = Constants?.expoConfig?.extra?.eas?.projectId;
|
|
51
|
+
const fromEasConfig = Constants?.easConfig?.projectId;
|
|
52
|
+
const fromEnv =
|
|
53
|
+
typeof process !== "undefined"
|
|
54
|
+
? String(process.env.EXPO_PUBLIC_EAS_PROJECT_ID ?? "").trim()
|
|
55
|
+
: "";
|
|
56
|
+
const projectId = String(fromExtra || fromEasConfig || fromEnv || "").trim();
|
|
57
|
+
return projectId || null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export type RegisterExpoPushTokenOptions = {
|
|
61
|
+
deviceId: string;
|
|
62
|
+
registerPushToken: (deviceId: string, token: string) => Promise<unknown>;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Request OS permission, fetch Expo push token, register it on the API.
|
|
67
|
+
* No-op on web, preview, missing peer, missing EAS projectId, or denied permission.
|
|
68
|
+
*/
|
|
69
|
+
export async function registerExpoPushToken(
|
|
70
|
+
options: RegisterExpoPushTokenOptions,
|
|
71
|
+
): Promise<string | null> {
|
|
72
|
+
if (Platform.OS === "web") return null;
|
|
73
|
+
|
|
74
|
+
const deviceId = options.deviceId?.trim();
|
|
75
|
+
if (!deviceId) return null;
|
|
76
|
+
|
|
77
|
+
const Notifications = loadNotifications();
|
|
78
|
+
if (!Notifications) {
|
|
79
|
+
if (__DEV__) {
|
|
80
|
+
console.warn(
|
|
81
|
+
"[wasto] expo-notifications is not installed; push registration skipped",
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const projectId = resolveEasProjectId();
|
|
88
|
+
if (!projectId) {
|
|
89
|
+
if (__DEV__) {
|
|
90
|
+
console.warn(
|
|
91
|
+
"[wasto] EAS projectId missing (extra.eas.projectId or EXPO_PUBLIC_EAS_PROJECT_ID); push registration skipped",
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (!handlerConfigured) {
|
|
98
|
+
Notifications.setNotificationHandler({
|
|
99
|
+
handleNotification: async () => ({
|
|
100
|
+
shouldShowAlert: true,
|
|
101
|
+
shouldPlaySound: true,
|
|
102
|
+
shouldSetBadge: false,
|
|
103
|
+
shouldShowBanner: true,
|
|
104
|
+
shouldShowList: true,
|
|
105
|
+
}),
|
|
106
|
+
});
|
|
107
|
+
handlerConfigured = true;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (Platform.OS === "android") {
|
|
111
|
+
await Notifications.setNotificationChannelAsync("default", {
|
|
112
|
+
name: "default",
|
|
113
|
+
importance: Notifications.AndroidImportance.MAX,
|
|
114
|
+
vibrationPattern: [0, 250, 250, 250],
|
|
115
|
+
lightColor: "#FF231F7C",
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const { status: existingStatus } = await Notifications.getPermissionsAsync();
|
|
120
|
+
let finalStatus = existingStatus;
|
|
121
|
+
if (existingStatus !== "granted") {
|
|
122
|
+
const { status } = await Notifications.requestPermissionsAsync();
|
|
123
|
+
finalStatus = status;
|
|
124
|
+
}
|
|
125
|
+
if (finalStatus !== "granted") return null;
|
|
126
|
+
|
|
127
|
+
const push = await Notifications.getExpoPushTokenAsync({ projectId });
|
|
128
|
+
const token = push?.data?.trim();
|
|
129
|
+
if (!token) return null;
|
|
130
|
+
|
|
131
|
+
await options.registerPushToken(deviceId, token);
|
|
132
|
+
return token;
|
|
133
|
+
}
|