Skip to content

Commit

Permalink
Merge branch 'main' into AS-914-reading-streak-setting-api
Browse files Browse the repository at this point in the history
  • Loading branch information
rebelchris authored Jan 16, 2025
2 parents 92206b9 + 5c4129d commit 2cdd880
Show file tree
Hide file tree
Showing 5 changed files with 196 additions and 98 deletions.
251 changes: 182 additions & 69 deletions packages/shared/src/contexts/PushNotificationContext.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import type { ReactElement } from 'react';
import React, {
useRef,
useEffect,
createContext,
useCallback,
useContext,
useEffect,
useRef,
useState,
} from 'react';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import type OneSignal from 'react-onesignal';
import OneSignal from 'react-onesignal';
import type { NotificationPromptSource } from '../lib/log';
import { LogEvent } from '../lib/log';
import { checkIsExtension, disabledRefetch } from '../lib/func';
Expand All @@ -19,14 +19,13 @@ import { useLogContext } from './LogContext';
import type { SubscriptionCallback } from '../components/notifications/utils';

export interface PushNotificationsContextData {
OneSignal: typeof OneSignal;
isPushSupported: boolean;
isInitialized: boolean;
isSubscribed: boolean;
isLoading: boolean;
shouldOpenPopup: boolean;
onSourceChange: (source: string) => void;
logPermissionGranted: (source: NotificationPromptSource) => void;
shouldOpenPopup: () => boolean;
subscribe: (source: NotificationPromptSource) => Promise<boolean>;
unsubscribe: (source: NotificationPromptSource) => Promise<void>;
}

export const PushNotificationsContext =
Expand All @@ -35,54 +34,30 @@ export const PushNotificationsContext =
isInitialized: true,
isSubscribed: false,
isLoading: false,
shouldOpenPopup: true,
logPermissionGranted: null,
onSourceChange: null,
OneSignal: null,
shouldOpenPopup: () => true,
subscribe: null,
unsubscribe: null,
});

interface PushNotificationContextProviderProps {
children: ReactElement;
}

type ChangeEventHandler = Parameters<
typeof OneSignal.User.PushSubscription.addEventListener
typeof OneSignal.Notifications.addEventListener<'permissionChange'>
>[1];

/**
* This context provider should only be used in the webapp
*/
export function PushNotificationContextProvider({
function OneSignalSubProvider({
children,
}: PushNotificationContextProviderProps): ReactElement {
const isExtension = checkIsExtension();
const notificationSourceRef = useRef<string>();
const onSourceChange = useCallback((source) => {
notificationSourceRef.current = source;
}, []);
const [isSubscribed, setIsSubscribed] = useState(false);
const { user } = useAuthContext();
const { logEvent } = useLogContext();
const subscriptionCallback: SubscriptionCallback = (
isSubscribedNew,
source,
existing_permission,
) => {
if (isSubscribedNew) {
logEvent({
event_name: LogEvent.ClickEnableNotification,
extra: JSON.stringify({
origin: source || notificationSourceRef.current,
permission: 'granted',
...(existing_permission && { existing_permission }),
}),
});
}
};
const subscriptionCallbackRef =
useRef<SubscriptionCallback>(subscriptionCallback);
subscriptionCallbackRef.current = subscriptionCallback;
const isEnabled = !!user && !isTesting && !isExtension;
const sourceRef = useRef<string>();
const subscriptionCallbackRef = useRef<SubscriptionCallback>();

const isEnabled = !!user && !isTesting;

const key = generateQueryKey(RequestKey.OneSignal, user);
const client = useQueryClient();
const {
Expand All @@ -100,75 +75,213 @@ export function PushNotificationContextProvider({
return osr;
}

const OneSingalImport = (await import('react-onesignal')).default;
const OneSignalImport = (await import('react-onesignal')).default;

await OneSingalImport.init({
await OneSignalImport.init({
appId: process.env.NEXT_PUBLIC_ONESIGNAL_APP_ID,
serviceWorkerParam: { scope: '/push/onesignal/' },
serviceWorkerPath: '/push/onesignal/OneSignalSDKWorker.js',
});

await OneSingalImport.login(user.id);
await OneSignalImport.login(user.id);

setIsSubscribed(OneSingalImport.User.PushSubscription.optedIn);
setIsSubscribed(OneSignalImport.User.PushSubscription.optedIn);

return OneSingalImport;
return OneSignalImport;
},
enabled: isEnabled,
...disabledRefetch,
});

const isPushSupported =
!!globalThis.window?.Notification &&
OneSignalCache?.Notifications.isPushSupported();
const isPushSupported = OneSignalCache?.Notifications.isPushSupported();

subscriptionCallbackRef.current = async (
newPermission,
source,
existingPermission,
) => {
if (newPermission) {
await OneSignal.User.PushSubscription.optIn();
setIsSubscribed(true);

logEvent({
event_name: LogEvent.ClickEnableNotification,
extra: JSON.stringify({
origin: source || sourceRef.current,
provider: 'web',
permission: 'granted',
...(existingPermission && {
existing_permission: existingPermission,
}),
}),
});
}
};

const subscribe = useCallback(
async (source: NotificationPromptSource) => {
if (!OneSignalCache) {
return false;
}

const logPermissionGranted = useCallback(
(source) => subscriptionCallbackRef.current?.(true, source, true),
[],
sourceRef.current = source;
const { permission } = OneSignalCache.Notifications;
if (!permission) {
await OneSignalCache.Notifications.requestPermission();
} else {
await subscriptionCallbackRef.current?.(true, source, true);
}
return OneSignalCache.Notifications.permission;
},
[OneSignalCache],
);

const unsubscribe = useCallback(async () => {
if (!OneSignalCache) {
return;
}
await OneSignalCache.User.PushSubscription.optOut();
setIsSubscribed(false);
}, [OneSignalCache]);

useEffect(() => {
if (!OneSignalCache) {
return undefined;
}

const onChange: ChangeEventHandler = ({ current }) => {
setIsSubscribed(() => current.optedIn);
subscriptionCallbackRef.current?.(current.optedIn);
const onChange: ChangeEventHandler = (permission) => {
subscriptionCallbackRef.current?.(permission);
};

OneSignalCache.User.PushSubscription.addEventListener('change', onChange);
OneSignalCache.Notifications.addEventListener('permissionChange', onChange);
return () => {
OneSignalCache.User.PushSubscription.removeEventListener(
'change',
OneSignalCache.Notifications.removeEventListener(
'permissionChange',
onChange,
);
};
}, [OneSignalCache]);

if (isExtension) {
throw new Error(
'PushNotificationContextProvider should only be used in the webapp',
);
}

return (
<PushNotificationsContext.Provider
value={{
isInitialized: !isEnabled || isFetched || !isSuccess,
isLoading,
isSubscribed,
isPushSupported: !!(isPushSupported && isSuccess && isEnabled),
onSourceChange,
logPermissionGranted,
shouldOpenPopup: false,
OneSignal: isEnabled && isFetched ? OneSignalCache : null,
isPushSupported: isPushSupported && isSuccess && isEnabled,
shouldOpenPopup: () => {
const { permission } = globalThis.Notification ?? {};
return permission === 'denied';
},
subscribe,
unsubscribe,
}}
>
{children}
</PushNotificationsContext.Provider>
);
}

function NativeAppleSubProvider({
children,
}: PushNotificationContextProviderProps): ReactElement {
const [isSubscribed, setIsSubscribed] = useState(false);
const { user } = useAuthContext();
const { logEvent } = useLogContext();
const isEnabled = !!user && !isTesting;

const key = generateQueryKey(RequestKey.ApplePush, user);
const { isFetched, isLoading, isSuccess } = useQuery<void>({
queryKey: key,

queryFn: async () => {
return new Promise((resolve) => {
globalThis.addEventListener(
'push-state',
(event: CustomEvent) => {
setIsSubscribed(!!event?.detail);
resolve();
},
{ once: true },
);
globalThis.webkit.messageHandlers['push-state'].postMessage(null);
globalThis.webkit.messageHandlers['push-user-id'].postMessage(user.id);
});
},
enabled: isEnabled,
...disabledRefetch,
});

const subscribe = useCallback(
async (source: NotificationPromptSource) => {
return new Promise<boolean>((resolve) => {
globalThis.addEventListener(
'push-subscribe',
(event: CustomEvent) => {
const subscribed = !!event?.detail;
setIsSubscribed(subscribed);
if (subscribed) {
logEvent({
event_name: LogEvent.ClickEnableNotification,
extra: JSON.stringify({
origin: source,
provider: 'apple',
permission: 'granted',
}),
});
}
resolve(subscribed);
},
{ once: true },
);
globalThis.webkit.messageHandlers['push-subscribe'].postMessage(null);
});
},
[logEvent],
);

const unsubscribe = useCallback(async () => {
globalThis.webkit.messageHandlers['push-unsubscribe'].postMessage(null);
setIsSubscribed(false);
}, []);

return (
<PushNotificationsContext.Provider
value={{
isInitialized: isFetched || !isSuccess,
isLoading,
isSubscribed,
isPushSupported: true,
shouldOpenPopup: () => false,
subscribe,
unsubscribe,
}}
>
{children}
</PushNotificationsContext.Provider>
);
}

/**
* This context provider should only be used in the webapp
*/
export function PushNotificationContextProvider({
children,
}: PushNotificationContextProviderProps): ReactElement {
const isExtension = checkIsExtension();

if (isExtension) {
throw new Error(
'PushNotificationContextProvider should only be used in the webapp',
);
}

if (globalThis.webkit && globalThis.webkit.messageHandlers) {
return <NativeAppleSubProvider>{children}</NativeAppleSubProvider>;
}

return <OneSignalSubProvider>{children}</OneSignalSubProvider>;
}

export const usePushNotificationContext = (): PushNotificationsContextData =>
useContext(PushNotificationsContext);
Loading

0 comments on commit 2cdd880

Please sign in to comment.