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); } }