169 lines
5.3 KiB
TypeScript
169 lines
5.3 KiB
TypeScript
import {
|
|
registerFcmToken,
|
|
deleteFcmToken,
|
|
storageGet,
|
|
storageSet,
|
|
storageRemove,
|
|
} from '../lib/shared';
|
|
import { Capacitor, registerPlugin } from '@capacitor/core';
|
|
|
|
export const FCM_CHANNEL_ID = 'goldenchart_trade_signals';
|
|
export const FCM_USER_OPT_IN_KEY = 'gc_fcm_user_opt_in';
|
|
export const FCM_REGISTERED_KEY = 'gc_fcm_registered';
|
|
|
|
export interface FcmPayload {
|
|
type?: string;
|
|
market?: string;
|
|
signalId?: string;
|
|
signalType?: string;
|
|
price?: string;
|
|
}
|
|
|
|
type FcmHandlers = {
|
|
onForeground?: (title: string, body: string, data?: FcmPayload) => void;
|
|
onTap?: (data: FcmPayload) => void;
|
|
};
|
|
|
|
interface GoldenFcmPlugin {
|
|
checkPermissions(): Promise<{ receive: string }>;
|
|
requestPermissions(): Promise<{ receive: string }>;
|
|
register(): Promise<{ value: string }>;
|
|
unregister(): Promise<void>;
|
|
addListener(
|
|
event: 'registration',
|
|
listener: (data: { value: string }) => void,
|
|
): Promise<{ remove: () => void }>;
|
|
addListener(
|
|
event: 'registrationError',
|
|
listener: (data: { error?: string }) => void,
|
|
): Promise<{ remove: () => void }>;
|
|
addListener(
|
|
event: 'pushNotificationReceived',
|
|
listener: (data: { title?: string; body?: string; data?: FcmPayload }) => void,
|
|
): Promise<{ remove: () => void }>;
|
|
addListener(
|
|
event: 'pushNotificationActionPerformed',
|
|
listener: (data: { notification?: { data?: FcmPayload } }) => void,
|
|
): Promise<{ remove: () => void }>;
|
|
}
|
|
|
|
const GoldenFcm = registerPlugin<GoldenFcmPlugin>('GoldenFcm');
|
|
|
|
let tokenRegistered = false;
|
|
let listenersAttached = false;
|
|
|
|
async function userOptedIn(): Promise<boolean> {
|
|
return (await storageGet(FCM_USER_OPT_IN_KEY)) === '1';
|
|
}
|
|
|
|
export async function markFcmUserOptIn(): Promise<void> {
|
|
await storageSet(FCM_USER_OPT_IN_KEY, '1');
|
|
}
|
|
|
|
async function attachListeners(handlers: {
|
|
onForeground?: (title: string, body: string, data?: FcmPayload) => void;
|
|
onTap?: (data: FcmPayload) => void;
|
|
}): Promise<void> {
|
|
if (listenersAttached || Capacitor.getPlatform() !== 'android') return;
|
|
listenersAttached = true;
|
|
|
|
await GoldenFcm.addListener('registration', async data => {
|
|
try {
|
|
await registerFcmToken(data.value);
|
|
await storageSet(FCM_REGISTERED_KEY, '1');
|
|
} catch (e) {
|
|
console.warn('[FCM] token register failed', e);
|
|
}
|
|
});
|
|
|
|
await GoldenFcm.addListener('registrationError', err => {
|
|
console.warn('[FCM] registration error', err);
|
|
void storageRemove(FCM_REGISTERED_KEY);
|
|
});
|
|
|
|
await GoldenFcm.addListener('pushNotificationReceived', notification => {
|
|
const title = notification.title ?? 'GoldenChart';
|
|
const body = notification.body ?? '';
|
|
handlers.onForeground?.(title, body, notification.data as FcmPayload);
|
|
});
|
|
|
|
await GoldenFcm.addListener('pushNotificationActionPerformed', action => {
|
|
handlers.onTap?.(action.notification?.data as FcmPayload);
|
|
});
|
|
}
|
|
|
|
/** ① 알림 권한만 (FCM 미호출 — 허용 직후 크래시 방지) */
|
|
export async function requestNotificationPermissionOnly(): Promise<boolean> {
|
|
if (Capacitor.getPlatform() !== 'android') return false;
|
|
await markFcmUserOptIn();
|
|
const perm = await GoldenFcm.checkPermissions();
|
|
if (perm.receive === 'granted') return true;
|
|
const req = await GoldenFcm.requestPermissions();
|
|
return req.receive === 'granted';
|
|
}
|
|
|
|
/** ② 권한 허용 후 FCM 토큰 등록 */
|
|
export async function registerFcmTokenOnly(): Promise<boolean> {
|
|
if (Capacitor.getPlatform() !== 'android') return false;
|
|
if (!(await userOptedIn())) return false;
|
|
|
|
const perm = await GoldenFcm.checkPermissions();
|
|
if (perm.receive !== 'granted') return false;
|
|
|
|
try {
|
|
await attachListeners({});
|
|
if (tokenRegistered) return true;
|
|
|
|
await new Promise(r => setTimeout(r, 800));
|
|
const result = await GoldenFcm.register();
|
|
tokenRegistered = true;
|
|
if (result?.value) {
|
|
await registerFcmToken(result.value);
|
|
await storageSet(FCM_REGISTERED_KEY, '1');
|
|
}
|
|
return !!result?.value;
|
|
} catch (e) {
|
|
console.warn('[FCM] register failed', e);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
export async function isFcmRegisteredOnDevice(): Promise<boolean> {
|
|
return (await storageGet(FCM_REGISTERED_KEY)) === '1';
|
|
}
|
|
|
|
/** @deprecated — requestNotificationPermissionOnly + registerFcmTokenOnly 사용 */
|
|
export async function initFcmPush(): Promise<boolean> {
|
|
const ok = await requestNotificationPermissionOnly();
|
|
if (!ok) return false;
|
|
await new Promise(r => setTimeout(r, 1000));
|
|
return registerFcmTokenOnly();
|
|
}
|
|
|
|
export async function unregisterFcmPush(): Promise<void> {
|
|
if (!Capacitor.isNativePlatform()) return;
|
|
try {
|
|
await deleteFcmToken();
|
|
if (Capacitor.getPlatform() === 'android' && (await userOptedIn())) {
|
|
await GoldenFcm.unregister();
|
|
}
|
|
} catch { /* ignore */ }
|
|
tokenRegistered = false;
|
|
listenersAttached = false;
|
|
await storageRemove(FCM_REGISTERED_KEY);
|
|
await storageRemove(FCM_USER_OPT_IN_KEY);
|
|
}
|
|
|
|
export async function checkPushPermission(): Promise<'granted' | 'denied' | 'prompt' | 'unknown'> {
|
|
if (!Capacitor.isNativePlatform()) return 'unknown';
|
|
try {
|
|
if (Capacitor.getPlatform() !== 'android') return 'unknown';
|
|
const perm = await GoldenFcm.checkPermissions();
|
|
if (perm.receive === 'granted') return 'granted';
|
|
if (perm.receive === 'denied') return 'denied';
|
|
return 'prompt';
|
|
} catch {
|
|
return 'unknown';
|
|
}
|
|
}
|