Merge pull request 'API notify deep link' (#238) from api-notify-deep-link into master
Reviewed-on: #238
This commit was merged in pull request #238.
This commit is contained in:
@@ -85,6 +85,9 @@ public class MainActivity extends BridgeActivity {
|
|||||||
|
|
||||||
// Handle share intent if app was launched from share sheet
|
// Handle share intent if app was launched from share sheet
|
||||||
handleShareIntent(getIntent());
|
handleShareIntent(getIntent());
|
||||||
|
// A notification tap that started the process is this first intent.
|
||||||
|
// Capacitor does not call onNewIntent for it.
|
||||||
|
deliverLaunchNotificationTap();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -94,6 +97,31 @@ public class MainActivity extends BridgeActivity {
|
|||||||
handleShareIntent(intent);
|
handleShareIntent(intent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Forward a cold-start notification tap into PushNotificationsPlugin.
|
||||||
|
* The plugin emits pushNotificationActionPerformed only from onNewIntent, and
|
||||||
|
* that is not called for the intent that created the activity. The message id
|
||||||
|
* is removed after the synchronous handoff so restoring this same intent does
|
||||||
|
* not open New Activity again.
|
||||||
|
*/
|
||||||
|
private void deliverLaunchNotificationTap() {
|
||||||
|
Intent intent = getIntent();
|
||||||
|
if (intent == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Bundle extras = intent.getExtras();
|
||||||
|
if (extras == null || !extras.containsKey("google.message_id")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (getBridge() == null) {
|
||||||
|
Log.w(TAG, "Bridge unavailable; launch notification tap was not delivered");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Log.d(TAG, "Delivering launch notification tap");
|
||||||
|
getBridge().onNewIntent(intent);
|
||||||
|
extras.remove("google.message_id");
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle share intents (ACTION_SEND or ACTION_SEND_MULTIPLE)
|
* Handle share intents (ACTION_SEND or ACTION_SEND_MULTIPLE)
|
||||||
* Processes shared images and stores them in SharedPreferences for plugin to read
|
* Processes shared images and stores them in SharedPreferences for plugin to read
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ import UserNotifications
|
|||||||
class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate {
|
class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate {
|
||||||
|
|
||||||
var window: UIWindow?
|
var window: UIWindow?
|
||||||
|
/// Remote-notification tap received before the Capacitor bridge could take it.
|
||||||
|
private var pendingPushTap: UNNotificationResponse?
|
||||||
|
private var pushTapFlushScheduled = false
|
||||||
|
|
||||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
||||||
// New Activity / dual schedule: plugin requires a registered native fetcher before configureNativeFetcher (parity with Android setNativeFetcher).
|
// New Activity / dual schedule: plugin requires a registered native fetcher before configureNativeFetcher (parity with Android setNativeFetcher).
|
||||||
@@ -96,8 +99,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
|
|||||||
func applicationDidBecomeActive(_ application: UIApplication) {
|
func applicationDidBecomeActive(_ application: UIApplication) {
|
||||||
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
|
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
|
||||||
|
|
||||||
// Re-set notification delegate when app becomes active (in case Capacitor resets it)
|
// Re-set notification delegate when app becomes active (in case Capacitor resets it).
|
||||||
|
// Daily reminders depend on willPresent staying here. Push taps are forwarded below.
|
||||||
UNUserNotificationCenter.current().delegate = self
|
UNUserNotificationCenter.current().delegate = self
|
||||||
|
flushPendingPushTap()
|
||||||
|
|
||||||
// Check for shared image from Share Extension when app becomes active
|
// Check for shared image from Share Extension when app becomes active
|
||||||
checkForSharedImageOnActivation()
|
checkForSharedImageOnActivation()
|
||||||
@@ -131,10 +136,76 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Handle notification tap/interaction.
|
/// Handle notification tap/interaction.
|
||||||
|
/// Local notifications (daily reminders) stay here. Remote notification taps are
|
||||||
|
/// forwarded to Capacitor, including a cold start where the bridge is not ready yet.
|
||||||
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
|
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
|
||||||
|
if response.notification.request.trigger is UNPushNotificationTrigger {
|
||||||
|
deliverOrQueuePushTap(response)
|
||||||
|
}
|
||||||
completionHandler()
|
completionHandler()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// AppDelegate owns UNUserNotificationCenter so daily-reminder foreground delivery
|
||||||
|
/// keeps working. Capacitor's push listener only runs if we hand it the tap.
|
||||||
|
private func deliverOrQueuePushTap(_ response: UNNotificationResponse) {
|
||||||
|
if deliverPushTapToCapacitor(response) {
|
||||||
|
pendingPushTap = nil
|
||||||
|
return
|
||||||
|
}
|
||||||
|
pendingPushTap = response
|
||||||
|
schedulePendingPushTapFlush()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func flushPendingPushTap() {
|
||||||
|
guard let pending = pendingPushTap else { return }
|
||||||
|
if deliverPushTapToCapacitor(pending) {
|
||||||
|
pendingPushTap = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func schedulePendingPushTapFlush() {
|
||||||
|
if pushTapFlushScheduled { return }
|
||||||
|
pushTapFlushScheduled = true
|
||||||
|
var attempts = 0
|
||||||
|
func attempt() {
|
||||||
|
attempts += 1
|
||||||
|
self.flushPendingPushTap()
|
||||||
|
if self.pendingPushTap == nil {
|
||||||
|
self.pushTapFlushScheduled = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if attempts < 5 {
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + Double(attempts) * 0.5) {
|
||||||
|
attempt()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.pushTapFlushScheduled = false
|
||||||
|
print("[AppDelegate] Push notification tap still pending; Capacitor bridge was not ready")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||||
|
attempt()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func deliverPushTapToCapacitor(_ response: UNNotificationResponse) -> Bool {
|
||||||
|
if !Thread.isMainThread {
|
||||||
|
var delivered = false
|
||||||
|
DispatchQueue.main.sync {
|
||||||
|
delivered = self.deliverPushTapToCapacitor(response)
|
||||||
|
}
|
||||||
|
return delivered
|
||||||
|
}
|
||||||
|
guard let window = self.window,
|
||||||
|
let bridgeVC = window.rootViewController as? CAPBridgeViewController,
|
||||||
|
let bridge = bridgeVC.bridge,
|
||||||
|
let handler = bridge.notificationRouter.pushNotificationHandler else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
handler.didReceive(response: response)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check for shared image when app launches or becomes active
|
* Check for shared image when app launches or becomes active
|
||||||
* This allows the app to detect shared images without requiring a deep link
|
* This allows the app to detect shared images without requiring a deep link
|
||||||
|
|||||||
@@ -543,8 +543,9 @@ export class NativeNotificationService implements NotificationServiceInterface {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Capacitor push delivery hook. Legacy WAKEUP_PING → /notifications/refresh
|
* Capacitor push delivery hook. Legacy WAKEUP_PING → /notifications/refresh
|
||||||
* consumption was retired; AlertSearch uses visible FCM notifications and is
|
* consumption was retired. AlertSearch is not opened here: a tap is a separate
|
||||||
* not handled here. Keep logging for diagnostics without scheduling api_* work.
|
* event and opens `/deep-link/new-activity` from the action listener. Keep
|
||||||
|
* logging for diagnostics without scheduling api_* work.
|
||||||
*/
|
*/
|
||||||
export async function handleCapacitorPushNotificationReceived(
|
export async function handleCapacitorPushNotificationReceived(
|
||||||
notification: PushNotificationSchema,
|
notification: PushNotificationSchema,
|
||||||
|
|||||||
@@ -0,0 +1,179 @@
|
|||||||
|
jest.mock("@/services/QRNavigationService", () => ({
|
||||||
|
QRNavigationService: {
|
||||||
|
getInstance: () => ({
|
||||||
|
getQRScannerRoute: () => ({ name: "contact-qr" }),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock("@/utils/logger", () => ({
|
||||||
|
logger: {
|
||||||
|
debug: jest.fn(),
|
||||||
|
error: jest.fn(),
|
||||||
|
info: jest.fn(),
|
||||||
|
warn: jest.fn(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
import { DeepLinkHandler } from "@/services/deepLinks";
|
||||||
|
import {
|
||||||
|
alertSearchDeepLinkForTap,
|
||||||
|
openAlertSearchFromNotificationTap,
|
||||||
|
resetAlertSearchNotificationTapState,
|
||||||
|
type PushNotificationTap,
|
||||||
|
} from "./alertSearchNotificationTap";
|
||||||
|
|
||||||
|
const CURSORS = "1234567.45678-1234567";
|
||||||
|
|
||||||
|
function alertSearchTap(
|
||||||
|
overrides: PushNotificationTap = {},
|
||||||
|
): PushNotificationTap {
|
||||||
|
return {
|
||||||
|
actionId: "tap",
|
||||||
|
notification: {
|
||||||
|
id: "msg-1",
|
||||||
|
data: { type: "alert_search", c: CURSORS },
|
||||||
|
},
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("AlertSearch notification tap routing", () => {
|
||||||
|
const openDeepLink = jest.fn<Promise<void>, [string]>();
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
resetAlertSearchNotificationTapState();
|
||||||
|
openDeepLink.mockReset();
|
||||||
|
openDeepLink.mockResolvedValue(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("opens the in-app deep link for /deep-link/new-activity, including start cursors", async () => {
|
||||||
|
const opened = await openAlertSearchFromNotificationTap(
|
||||||
|
alertSearchTap(),
|
||||||
|
openDeepLink,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(opened).toBe(true);
|
||||||
|
expect(openDeepLink).toHaveBeenCalledWith(
|
||||||
|
`timesafari://new-activity?c=${CURSORS}`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("omits c when the payload has no start cursors", () => {
|
||||||
|
expect(
|
||||||
|
alertSearchDeepLinkForTap(
|
||||||
|
alertSearchTap({
|
||||||
|
notification: { id: "msg-1", data: { type: "alert_search" } },
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
).toBe("timesafari://new-activity");
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
["another FCM type", { type: "daily_reminder", c: CURSORS }],
|
||||||
|
["a payload with no type", { c: CURSORS }],
|
||||||
|
["a missing payload", undefined],
|
||||||
|
])("does not navigate for %s", async (_label, data) => {
|
||||||
|
const opened = await openAlertSearchFromNotificationTap(
|
||||||
|
alertSearchTap({ notification: { id: "other", data } }),
|
||||||
|
openDeepLink,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(opened).toBe(false);
|
||||||
|
expect(openDeepLink).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not navigate when the notification is dismissed", async () => {
|
||||||
|
const opened = await openAlertSearchFromNotificationTap(
|
||||||
|
alertSearchTap({ actionId: "dismiss" }),
|
||||||
|
openDeepLink,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(opened).toBe(false);
|
||||||
|
expect(openDeepLink).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("opens once when a cold-start check and the tap listener both see the same tap", async () => {
|
||||||
|
const tap = alertSearchTap();
|
||||||
|
|
||||||
|
const first = await openAlertSearchFromNotificationTap(
|
||||||
|
tap,
|
||||||
|
openDeepLink,
|
||||||
|
1_000,
|
||||||
|
);
|
||||||
|
const second = await openAlertSearchFromNotificationTap(
|
||||||
|
tap,
|
||||||
|
openDeepLink,
|
||||||
|
2_000,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(first).toBe(true);
|
||||||
|
expect(second).toBe(false);
|
||||||
|
expect(openDeepLink).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("opens a later tap of the same notification after the dedupe window", async () => {
|
||||||
|
const tap = alertSearchTap();
|
||||||
|
|
||||||
|
await openAlertSearchFromNotificationTap(tap, openDeepLink, 1_000);
|
||||||
|
const later = await openAlertSearchFromNotificationTap(
|
||||||
|
tap,
|
||||||
|
openDeepLink,
|
||||||
|
1_000 + 5_000,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(later).toBe(true);
|
||||||
|
expect(openDeepLink).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("still opens a different notification that arrives immediately after", async () => {
|
||||||
|
await openAlertSearchFromNotificationTap(
|
||||||
|
alertSearchTap(),
|
||||||
|
openDeepLink,
|
||||||
|
1_000,
|
||||||
|
);
|
||||||
|
const other = await openAlertSearchFromNotificationTap(
|
||||||
|
alertSearchTap({
|
||||||
|
notification: {
|
||||||
|
id: "msg-2",
|
||||||
|
data: { type: "alert_search", c: "99.99" },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
openDeepLink,
|
||||||
|
1_100,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(other).toBe(true);
|
||||||
|
expect(openDeepLink).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("lets a failed open be retried", async () => {
|
||||||
|
openDeepLink.mockRejectedValueOnce(new Error("router not ready"));
|
||||||
|
const tap = alertSearchTap();
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
openAlertSearchFromNotificationTap(tap, openDeepLink, 1_000),
|
||||||
|
).rejects.toThrow("router not ready");
|
||||||
|
const retried = await openAlertSearchFromNotificationTap(
|
||||||
|
tap,
|
||||||
|
openDeepLink,
|
||||||
|
1_100,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(retried).toBe(true);
|
||||||
|
expect(openDeepLink).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("resolves that deep link to the New Activity route with c", async () => {
|
||||||
|
const router = { replace: jest.fn().mockResolvedValue(undefined) };
|
||||||
|
const handler = new DeepLinkHandler(router as never);
|
||||||
|
|
||||||
|
await handler.handleDeepLink(`timesafari://new-activity?c=${CURSORS}`);
|
||||||
|
|
||||||
|
expect(router.replace).toHaveBeenCalledWith({
|
||||||
|
name: "new-activity",
|
||||||
|
params: {},
|
||||||
|
query: { c: CURSORS },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
/**
|
||||||
|
* Routes a tap on the daily New Activity FCM notification to the in-app form of
|
||||||
|
* `/deep-link/new-activity`.
|
||||||
|
*
|
||||||
|
* Arrival (`pushNotificationReceived`) must not call this. Foreground, background
|
||||||
|
* resume, and cold start all arrive as `pushNotificationActionPerformed`: Capacitor
|
||||||
|
* retains that event until the listener exists, and the native shells forward the
|
||||||
|
* launch intent into the same event. A launch check and the listener can both
|
||||||
|
* see one tap, so the same notification is opened once.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const ALERT_SEARCH_FCM_TYPE = "alert_search";
|
||||||
|
/** Long enough to cover the startup listener registering after a launch forward. */
|
||||||
|
const DEDUPE_WINDOW_MS = 5000;
|
||||||
|
|
||||||
|
const recentTapAt = new Map<string, number>();
|
||||||
|
|
||||||
|
export interface PushNotificationTap {
|
||||||
|
actionId?: string;
|
||||||
|
notification?: {
|
||||||
|
id?: string;
|
||||||
|
data?: unknown;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||||
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isNotificationBodyTap(actionId: string | undefined): boolean {
|
||||||
|
return actionId === undefined || actionId === "" || actionId === "tap";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `timesafari://new-activity` is the in-app deep link for `/deep-link/new-activity`.
|
||||||
|
* Optional `c` is the notification's start cursors.
|
||||||
|
*/
|
||||||
|
export function alertSearchDeepLinkForTap(
|
||||||
|
tap: PushNotificationTap,
|
||||||
|
): string | null {
|
||||||
|
if (!isNotificationBodyTap(tap.actionId)) return null;
|
||||||
|
const data = tap.notification?.data;
|
||||||
|
if (!isRecord(data) || data.type !== ALERT_SEARCH_FCM_TYPE) return null;
|
||||||
|
const c =
|
||||||
|
typeof data.c === "string" && data.c.length > 0 ? data.c : undefined;
|
||||||
|
return c
|
||||||
|
? `timesafari://new-activity?c=${encodeURIComponent(c)}`
|
||||||
|
: "timesafari://new-activity";
|
||||||
|
}
|
||||||
|
|
||||||
|
function dedupeKey(tap: PushNotificationTap, url: string): string {
|
||||||
|
const id = tap.notification?.id?.trim();
|
||||||
|
return id ? `id:${id}` : `url:${url}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function claimTap(key: string, now: number): boolean {
|
||||||
|
const previous = recentTapAt.get(key);
|
||||||
|
if (previous !== undefined && now - previous < DEDUPE_WINDOW_MS) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
recentTapAt.set(key, now);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Clears the tap dedupe window. Tests use this between cases. */
|
||||||
|
export function resetAlertSearchNotificationTapState(): void {
|
||||||
|
recentTapAt.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open `/deep-link/new-activity` for an AlertSearch notification tap.
|
||||||
|
* Returns false when the action is not that tap, or when this tap was already opened.
|
||||||
|
*/
|
||||||
|
export async function openAlertSearchFromNotificationTap(
|
||||||
|
tap: PushNotificationTap,
|
||||||
|
openDeepLink: (url: string) => Promise<void>,
|
||||||
|
now: number = Date.now(),
|
||||||
|
): Promise<boolean> {
|
||||||
|
const url = alertSearchDeepLinkForTap(tap);
|
||||||
|
if (!url) return false;
|
||||||
|
const key = dedupeKey(tap, url);
|
||||||
|
if (!claimTap(key, now)) return false;
|
||||||
|
try {
|
||||||
|
await openDeepLink(url);
|
||||||
|
} catch (error) {
|
||||||
|
recentTapAt.delete(key);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
@@ -22,6 +22,7 @@ import {
|
|||||||
} from "firebase/messaging";
|
} from "firebase/messaging";
|
||||||
import { logger } from "@/utils/logger";
|
import { logger } from "@/utils/logger";
|
||||||
import { NotificationInspector } from "@/plugins/NotificationInspectorPlugin";
|
import { NotificationInspector } from "@/plugins/NotificationInspectorPlugin";
|
||||||
|
import { openAlertSearchFromNotificationTap } from "./alertSearchNotificationTap";
|
||||||
import { handleCapacitorPushNotificationReceived } from "./NativeNotificationService";
|
import { handleCapacitorPushNotificationReceived } from "./NativeNotificationService";
|
||||||
import { getNotificationApiHeaders } from "./notificationApiAuth";
|
import { getNotificationApiHeaders } from "./notificationApiAuth";
|
||||||
import { deferFcmRegistration } from "./notificationAuthLifecycle";
|
import { deferFcmRegistration } from "./notificationAuthLifecycle";
|
||||||
@@ -268,21 +269,16 @@ async function attachFirebaseMessagingIfSupported(
|
|||||||
* Native: register Capacitor push listeners, request permissions, register for push,
|
* Native: register Capacitor push listeners, request permissions, register for push,
|
||||||
* then initialize Firebase Messaging when env config and platform support allow.
|
* then initialize Firebase Messaging when env config and platform support allow.
|
||||||
*/
|
*/
|
||||||
/** The FCM data `type` of the daily New Activity notification. */
|
|
||||||
const ALERT_SEARCH_FCM_TYPE = "alert_search";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Open New Activity when the daily New Activity notification is tapped,
|
* Open `/deep-link/new-activity` through the deep-link handler.
|
||||||
* passing its start cursors (`c`) so the screen can compare them with its own.
|
* The router is imported here because it loads views that import this module's callers.
|
||||||
*/
|
*/
|
||||||
async function openNewActivityForAlertSearch(
|
async function openNewActivityDeepLink(url: string): Promise<void> {
|
||||||
data: Record<string, unknown> | undefined,
|
|
||||||
): Promise<void> {
|
|
||||||
if (data?.type !== ALERT_SEARCH_FCM_TYPE) return;
|
|
||||||
// imported here: the router imports views that import this module's callers
|
|
||||||
const { default: router } = await import("@/router");
|
const { default: router } = await import("@/router");
|
||||||
const c = typeof data.c === "string" ? data.c : undefined;
|
const { DeepLinkHandler } = await import("@/services/deepLinks");
|
||||||
await router.push({ name: "new-activity", query: c ? { c } : {} });
|
await router.isReady();
|
||||||
|
await new DeepLinkHandler(router).handleDeepLink(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function initializeNativePushAndFirebaseMessagingImpl(): Promise<void> {
|
async function initializeNativePushAndFirebaseMessagingImpl(): Promise<void> {
|
||||||
@@ -328,16 +324,21 @@ async function initializeNativePushAndFirebaseMessagingImpl(): Promise<void> {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Tap only. pushNotificationReceived (above) stays a log. Cold start is this
|
||||||
|
// same event: native code forwards the launch tap, and Capacitor retains it
|
||||||
|
// until this listener exists. openAlertSearchFromNotificationTap ignores a
|
||||||
|
// second delivery of that tap.
|
||||||
await PushNotifications.addListener(
|
await PushNotifications.addListener(
|
||||||
"pushNotificationActionPerformed",
|
"pushNotificationActionPerformed",
|
||||||
(action) => {
|
(action) => {
|
||||||
logger.debug(`${LOG} pushNotificationActionPerformed`, action);
|
logger.debug(`${LOG} pushNotificationActionPerformed`, action);
|
||||||
logPushNotificationActionPerformed(action);
|
logPushNotificationActionPerformed(action);
|
||||||
void openNewActivityForAlertSearch(action.notification?.data).catch(
|
void openAlertSearchFromNotificationTap(
|
||||||
(err) => {
|
action,
|
||||||
|
openNewActivityDeepLink,
|
||||||
|
).catch((err) => {
|
||||||
logger.warn(`${LOG} Could not open New Activity`, err);
|
logger.warn(`${LOG} Could not open New Activity`, err);
|
||||||
},
|
});
|
||||||
);
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user