From 941bfec19146d74f0d0f5665df3720ec12e88e13 Mon Sep 17 00:00:00 2001 From: xantus smith <281845000+smith-909@users.noreply.github.com> Date: Wed, 21 Oct 2026 07:11:29 -0700 Subject: [PATCH] fix(deeplink): deliver the deferred deep link once, on the launch that reports the install onDeferredDeepLink is called with the link's data, or null for an organic install, whether it is registered before or after init(). Later launches don't call it; getInstallData() still returns the install's link data. --- README.md | 2 +- __tests__/DeferredDeepLink.test.ts | 43 ++++++++++++++++++++++++++++++ src/DeferredDeepLink.ts | 27 +++++++++++++++++++ src/LinkFortySDK.ts | 35 +++++++++--------------- 4 files changed, 84 insertions(+), 23 deletions(-) create mode 100644 __tests__/DeferredDeepLink.test.ts create mode 100644 src/DeferredDeepLink.ts diff --git a/README.md b/README.md index 345baa3..bef2fd4 100644 --- a/README.md +++ b/README.md @@ -203,7 +203,7 @@ LinkForty.onDeepLink((url: string, data: DeepLinkData | null) => { ### `onDeferredDeepLink(callback)` -Listen for deferred deep links. Called on first launch after install if the user clicked a LinkForty link before installing. +Listen for deferred deep links. Called once, on the launch that reports the install: with the link's data if the user clicked a LinkForty link before installing, or `null` for an organic install. It can be registered before or after `init()`. Later launches don't call it; use `getInstallData()` for the install's link data. ```typescript LinkForty.onDeferredDeepLink((data: DeepLinkData | null) => { diff --git a/__tests__/DeferredDeepLink.test.ts b/__tests__/DeferredDeepLink.test.ts new file mode 100644 index 0000000..a0a6aa8 --- /dev/null +++ b/__tests__/DeferredDeepLink.test.ts @@ -0,0 +1,43 @@ +import { describe, it, expect, vi } from 'vitest'; +import { DeferredDeepLink } from '../src/DeferredDeepLink'; +import type { DeepLinkData } from '../src/types'; + +const data = { shortCode: 'abc123', linkId: 'link-1' } as DeepLinkData; + +describe('DeferredDeepLink', () => { + it('calls a callback registered before the install is reported', () => { + const deferred = new DeferredDeepLink(); + const callback = vi.fn(); + deferred.register(callback); + deferred.deliver(data); + + expect(callback).toHaveBeenCalledTimes(1); + expect(callback).toHaveBeenCalledWith(data); + }); + + it('calls a callback registered after the install is reported', () => { + const deferred = new DeferredDeepLink(); + deferred.deliver(data); + const callback = vi.fn(); + deferred.register(callback); + + expect(callback).toHaveBeenCalledWith(data); + }); + + it('delivers null for an organic install registered after init', () => { + const deferred = new DeferredDeepLink(); + deferred.deliver(null); + const callback = vi.fn(); + deferred.register(callback); + + expect(callback).toHaveBeenCalledWith(null); + }); + + it('calls nothing on a launch that does not report the install', () => { + const deferred = new DeferredDeepLink(); + const callback = vi.fn(); + deferred.register(callback); + + expect(callback).not.toHaveBeenCalled(); + }); +}); diff --git a/src/DeferredDeepLink.ts b/src/DeferredDeepLink.ts new file mode 100644 index 0000000..09effe9 --- /dev/null +++ b/src/DeferredDeepLink.ts @@ -0,0 +1,27 @@ +import type { DeepLinkData, DeferredDeepLinkCallback } from './types'; + +/** + * Delivers the deferred deep link once, on the launch that reports the install: + * the link's data, or null for an organic install. A callback registered after + * that is called with the same result; on later launches nothing is delivered. + * + * Kept free of React Native imports so it can be tested (see urlParameters.ts). + */ +export class DeferredDeepLink { + private callback: DeferredDeepLinkCallback | null = null; + private delivered = false; + private data: DeepLinkData | null = null; + + /** Registers the callback, replacing any earlier one. */ + register(callback: DeferredDeepLinkCallback): void { + this.callback = callback; + if (this.delivered) callback(this.data); + } + + /** Delivers the install's result. Called only on the launch that reports the install. */ + deliver(data: DeepLinkData | null): void { + this.data = data; + this.delivered = true; + this.callback?.(data); + } +} diff --git a/src/LinkFortySDK.ts b/src/LinkFortySDK.ts index ea92efd..503a179 100644 --- a/src/LinkFortySDK.ts +++ b/src/LinkFortySDK.ts @@ -8,6 +8,7 @@ import { DeepLinkHandler } from './DeepLinkHandler'; import { AttributionContext } from './AttributionContext'; import { NavigationTracker } from './NavigationTracker'; import { LinkDomains } from './LinkDomains'; +import { DeferredDeepLink } from './DeferredDeepLink'; import { SDK_NAME, SDK_VERSION } from './version'; import type { LinkFortyConfig, @@ -28,7 +29,8 @@ const STORAGE_KEYS = { export class LinkFortySDK { private config: LinkFortyConfig | null = null; private deepLinkHandler: DeepLinkHandler | null = null; - private deferredDeepLinkCallback: DeferredDeepLinkCallback | null = null; + /** Delivers the deferred deep link on the launch that reports the install */ + private deferredDeepLink = new DeferredDeepLink(); private installId: string | null = null; private externalUserId: string | null = null; private initialized: boolean = false; @@ -165,17 +167,13 @@ export class LinkFortySDK { } /** - * Register callback for deferred deep links (new installs) + * Register callback for deferred deep links, called once on the launch that + * reports the install: with the link's data, or null for an organic install. + * Can be registered before or after init(). Later launches don't call it; + * use getInstallData() for the install's link data. */ onDeferredDeepLink(callback: DeferredDeepLinkCallback): void { - this.deferredDeepLinkCallback = callback; - - // If we already have install data, call the callback immediately - this.getInstallData().then((data) => { - if (data && this.deferredDeepLinkCallback) { - this.deferredDeepLinkCallback(data); - } - }); + this.deferredDeepLink.register(callback); } /** @@ -533,10 +531,7 @@ export class LinkFortySDK { await this.attribution.recordDeepLinkOpen(deepLinkData.linkId); } - // Call deferred deep link callback if registered - if (this.deferredDeepLinkCallback) { - this.deferredDeepLinkCallback(deepLinkData); - } + this.deferredDeepLink.deliver(deepLinkData); if (this.config.debug) { console.log('[LinkForty] Install attributed with confidence:', response.confidenceScore); @@ -547,18 +542,14 @@ export class LinkFortySDK { console.log('[LinkForty] Install not attributed (organic install)'); } - // Call callback with null to indicate organic install - if (this.deferredDeepLinkCallback) { - this.deferredDeepLinkCallback(null); - } + // null indicates an organic install + this.deferredDeepLink.deliver(null); } } catch (error) { console.error('[LinkForty] Failed to report install:', error); - // Call callback with null on error - if (this.deferredDeepLinkCallback) { - this.deferredDeepLinkCallback(null); - } + // null on error + this.deferredDeepLink.deliver(null); } }