diff --git a/CHANGELOG.md b/CHANGELOG.md index 39a2f9f..923af9c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,45 @@ # Changelog +## 0.1.1 + +Implements [wrapper parity 0.1.1](https://github.com/linktrail-io/workspace/blob/main/contracts/wrapper-parity-0.1.1.md). +Native SDKs unchanged: iOS `LinkTrailSDK ~> 0.1.0`, Android `io.linktrail:sdk:0.1.0`. + +### Changed +- **Breaking: error codes are now camelCase**, matching the native `LinkTrailError` cases. Every + promise rejects with a `LinkTrailError` whose `message` is the native message unchanged; `server` + errors also carry `statusCode` and, if present, `body` (also on `onError` events). For this + release the old value is still available as `error.legacyCode`; it will be removed in the next + minor release. + + | Before (`code`, now `legacyCode`) | After (`code`) | + | --- | --- | + | `missing_api_key` | `missingApiKey` | + | `invalid_api_key` | `invalidApiKey` | + | `not_configured` | `notConfigured` | + | `invalid_url` | `invalidUrl` | + | `not_a_linktrail_url` | `notALinkTrailUrl` | + | `empty_response` | `emptyResponse` | + | `invalid_argument` | `invalidArgument` | + | `transport`, `server`, `decoding`, `unknown` | unchanged | + +- **Links and attribution are no longer lost before a listener exists.** The native `onLink` is + registered only when the first JS `onLink` listener subscribes, so a deferred link stays saved + natively until JS receives it. Re-engagement links resolved earlier are buffered (max 10, oldest + dropped) and delivered once, in order, to the first listener. `onAttribution` keeps the last + result and replays it to the first listener. Later listeners get no replays. `onLink` / + `onAttribution` now return a JS subscription (still `{ remove() }`). +- **Behavior change (Android):** `requestTrackingAuthorization()` now resolves `true` instead of + `false`. Android has no App Tracking Transparency, so the platform never restricts tracking; + this matches the Flutter SDK. It is not user consent — keep using `setConsent` / + `requireConsent`. iOS is unchanged. + +### Added +- `LinkTrail.version` (this package's version) and `LinkTrail.nativeSdkVersion` (the embedded + native SDK's version). +- `LinkTrailError` class and `LinkTrailErrorCode` type exports. +- A Jest test suite (`npm test`) covering the JS/TS layer with a mocked native module. + ## 0.1.0 Adopts the native **0.1.0** SDKs: iOS `LinkTrailSDK ~> 0.1.0` (from `0.0.11`) and Android diff --git a/README.md b/README.md index 6418df2..5af1b81 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ from Maven Central — so there's nothing else to add. ## Quick start ```ts -import LinkTrail from 'linktrail-react-native'; +import LinkTrail, { LinkTrailError } from 'linktrail-react-native'; // At app launch. The API key is required. await LinkTrail.configure('lt_live_…'); @@ -43,6 +43,30 @@ LinkTrail.onError((error) => console.warn(`[LinkTrail] ${error.code}: ${error.me The install is tracked automatically by `configure`, and incoming links (Universal Links / App Links / custom schemes) are forwarded via React Native's `Linking` API — no extra wiring. +`onLink` can be registered after a splash or login screen: links that arrive before the first +listener are kept and delivered to it once, in order (later listeners get no replays). A deferred +link stays saved natively (across launches) until a listener exists; up to 10 re-engagement links +are buffered in memory. `onAttribution` replays the latest result to its first listener. + +### Errors + +Every promise rejects with a `LinkTrailError`, and `onError` gets the same fields: + +| Field | | +| --- | --- | +| `code` | `missingApiKey`, `invalidApiKey`, `notConfigured`, `invalidUrl`, `notALinkTrailUrl`, `transport`, `server`, `decoding`, `emptyResponse`, `invalidArgument`, `unknown` | +| `message` | The native SDK's message, unchanged | +| `statusCode`, `body` | `server` errors only (`body` when the server sent one) | +| `legacyCode` | The pre-0.1.1 snake_case code. Deprecated; removed in the next minor release | + +```ts +try { + await LinkTrail.trackEvent('purchase'); +} catch (e) { + if (e instanceof LinkTrailError && e.code === 'server') console.warn(e.statusCode, e.body); +} +``` + ## More ```ts @@ -52,8 +76,14 @@ await LinkTrail.trackEvent('purchase', { value: 59.99, currency: 'USD' }); // Cached results: const attribution = await LinkTrail.getLastAttribution(); const lastLink = await LinkTrail.getLastDeepLink(); + +// Versions: this package, and the embedded native SDK. +LinkTrail.version; // "0.1.1" +LinkTrail.nativeSdkVersion; // e.g. "0.1.0" -// iOS ATT / SKAdNetwork (no-ops on Android): +// iOS ATT / SKAdNetwork. On Android there is no ATT: requestTrackingAuthorization() +// is not applicable and resolves `true` (the OS does not restrict tracking); the +// SKAdNetwork calls are no-ops. await LinkTrail.requestTrackingAuthorization(); LinkTrail.registerForSKAdAttribution(); LinkTrail.updateConversionValue(42, 'medium'); diff --git a/__tests__/index.test.ts b/__tests__/index.test.ts new file mode 100644 index 0000000..ccfaa84 --- /dev/null +++ b/__tests__/index.test.ts @@ -0,0 +1,380 @@ +/* eslint-env jest */ +import { Linking, Platform } from 'react-native'; + +type Listener = (payload: unknown) => void; + +const mockSub = () => ({ remove: jest.fn() }); + +const mockNative = { + configure: jest.fn(), + trackInstall: jest.fn(), + trackInstallWithClickToken: jest.fn(), + trackEvent: jest.fn(), + handleDeepLink: jest.fn(), + getLastAttribution: jest.fn(), + getLastDeepLink: jest.fn(), + requestTrackingAuthorization: jest.fn(), + registerForSKAdAttribution: jest.fn(), + updateConversionValue: jest.fn(), + setConsent: jest.fn(), + resetForTesting: jest.fn(), + startLinkDelivery: jest.fn(), + nativeSdkVersion: jest.fn(), + onLink: jest.fn(), + onAttribution: jest.fn(), + onError: jest.fn(), +}; + +jest.mock('../src/NativeLinkTrail', () => ({ + __esModule: true, + default: mockNative, +})); + +type Api = typeof import('../src/index'); +let api: Api; +let urlListener: ((e: { url: string }) => void) | undefined; +let linkingSub: { remove: jest.Mock }; + +function load(): Api { + let mod!: Api; + jest.isolateModules(() => { + mod = require('../src/index'); + }); + return mod; +} + +beforeEach(() => { + jest.resetAllMocks(); + mockNative.configure.mockResolvedValue(undefined); + mockNative.handleDeepLink.mockResolvedValue(null); + linkingSub = mockSub(); + urlListener = undefined; + jest.spyOn(Linking, 'getInitialURL').mockResolvedValue(null); + jest.spyOn(Linking, 'addEventListener').mockImplementation((( + _type: string, + handler: (e: { url: string }) => void + ) => { + urlListener = handler; + return linkingSub; + }) as never); + api = load(); +}); + +describe('API surface', () => { + it('exposes the documented methods on both named and default export', () => { + expect(api.default).toBe(api.LinkTrail); + for (const name of [ + 'configure', + 'onLink', + 'onAttribution', + 'onError', + 'trackInstall', + 'trackInstallWithClickToken', + 'trackEvent', + 'handleDeepLink', + 'getLastAttribution', + 'getLastDeepLink', + 'requestTrackingAuthorization', + 'registerForSKAdAttribution', + 'updateConversionValue', + 'setConsent', + 'resetForTesting', + ]) { + expect(typeof (api.LinkTrail as Record)[name]).toBe( + 'function' + ); + } + expect(typeof api.LinkTrailPasteButton).toBe('function'); + }); +}); + +describe('configure', () => { + it('passes the api key and options to native, minus autoHandleLinks', async () => { + await api.LinkTrail.configure('lt_live_x', { + requireConsent: false, + autoHandleLinks: true, + autoTrackInstall: false, + }); + expect(mockNative.configure).toHaveBeenCalledWith('lt_live_x', { + requireConsent: false, + autoTrackInstall: false, + }); + }); + + it('defaults options to an empty object', async () => { + await api.LinkTrail.configure('lt_live_x'); + expect(mockNative.configure).toHaveBeenCalledWith('lt_live_x', {}); + }); + + it('propagates native rejections (missingApiKey) and does not forward links', async () => { + const err = Object.assign(new Error('blank'), { code: 'missingApiKey' }); + mockNative.configure.mockRejectedValue(err); + await expect(api.LinkTrail.configure('')).rejects.toMatchObject({ + code: 'missingApiKey', + legacyCode: 'missing_api_key', + message: 'blank', + }); + expect(Linking.addEventListener).not.toHaveBeenCalled(); + expect(Linking.getInitialURL).not.toHaveBeenCalled(); + }); +}); + +describe('deep-link forwarding', () => { + it('forwards the initial URL and subsequent url events to native', async () => { + (Linking.getInitialURL as jest.Mock).mockResolvedValue('app://initial'); + await api.LinkTrail.configure('k'); + await Promise.resolve(); + expect(mockNative.handleDeepLink).toHaveBeenCalledWith('app://initial'); + + urlListener?.({ url: 'app://later' }); + expect(mockNative.handleDeepLink).toHaveBeenCalledWith('app://later'); + }); + + it('does not call native for a missing initial URL', async () => { + await api.LinkTrail.configure('k'); + await Promise.resolve(); + expect(mockNative.handleDeepLink).not.toHaveBeenCalled(); + }); + + it('swallows native errors from forwarded URLs', async () => { + (Linking.getInitialURL as jest.Mock).mockResolvedValue('app://x'); + mockNative.handleDeepLink.mockRejectedValue(new Error('not_a_link')); + await api.LinkTrail.configure('k'); + urlListener?.({ url: 'app://y' }); + await new Promise((r) => setImmediate(r)); + // No unhandled rejection: test completing is the assertion. + expect(mockNative.handleDeepLink).toHaveBeenCalledTimes(2); + }); + + it('subscribes once and forwards the initial URL once across repeated configure calls', async () => { + (Linking.getInitialURL as jest.Mock).mockResolvedValue('app://i'); + await api.LinkTrail.configure('k'); + await api.LinkTrail.configure('k'); + expect(Linking.addEventListener).toHaveBeenCalledTimes(1); + expect(Linking.getInitialURL).toHaveBeenCalledTimes(1); + }); + + it('autoHandleLinks: false does not subscribe and removes an existing subscription', async () => { + await api.LinkTrail.configure('k', { autoHandleLinks: false }); + expect(Linking.addEventListener).not.toHaveBeenCalled(); + + await api.LinkTrail.configure('k'); + expect(Linking.addEventListener).toHaveBeenCalledTimes(1); + await api.LinkTrail.configure('k', { autoHandleLinks: false }); + expect(linkingSub.remove).toHaveBeenCalledTimes(1); + }); + + it('handleDeepLink passes the url through and resolves link or null', async () => { + const link = { path: '/p' }; + mockNative.handleDeepLink.mockResolvedValueOnce(link); + await expect(api.LinkTrail.handleDeepLink('https://x/p')).resolves.toBe( + link + ); + expect(mockNative.handleDeepLink).toHaveBeenCalledWith('https://x/p'); + mockNative.handleDeepLink.mockResolvedValueOnce(null); + await expect(api.LinkTrail.handleDeepLink('https://other')).resolves.toBe( + null + ); + }); +}); + +describe('event emitters', () => { + it('onLink unwraps { link, source } into listener args and starts native delivery', () => { + let emit!: Listener; + mockNative.onLink.mockImplementation((l: Listener) => { + emit = l; + return mockSub(); + }); + const listener = jest.fn(); + api.LinkTrail.onLink(listener); + expect(mockNative.startLinkDelivery).toHaveBeenCalledTimes(1); + const link = { path: '/a' }; + emit({ link, source: 'deferred' }); + expect(listener).toHaveBeenCalledWith(link, 'deferred'); + emit({ link, source: 'reengagement' }); + expect(listener).toHaveBeenLastCalledWith(link, 'reengagement'); + }); + + it('onAttribution passes the payload through', () => { + let emit!: Listener; + mockNative.onAttribution.mockImplementation((l: Listener) => { + emit = l; + return mockSub(); + }); + const listener = jest.fn(); + api.LinkTrail.onAttribution(listener); + const payload = { attributed: true }; + emit(payload); + expect(listener).toHaveBeenCalledWith(payload); + }); + + it('onError passes the payload through and unsubscribe works', () => { + const sub = mockSub(); + let emit!: Listener; + mockNative.onError.mockImplementation((l: Listener) => { + emit = l; + return sub; + }); + const listener = jest.fn(); + const s = api.LinkTrail.onError(listener); + emit({ code: 'transport', message: 'offline' }); + expect(listener).toHaveBeenCalledWith({ + code: 'transport', + legacyCode: 'transport', + message: 'offline', + }); + s.remove(); + expect(sub.remove).toHaveBeenCalled(); + }); +}); + +describe('tracking calls', () => { + it('trackInstall defaults force to false and passes true through', async () => { + mockNative.trackInstall.mockResolvedValue({ attributed: false }); + await expect(api.LinkTrail.trackInstall()).resolves.toEqual({ + attributed: false, + }); + expect(mockNative.trackInstall).toHaveBeenLastCalledWith(false); + await api.LinkTrail.trackInstall(true); + expect(mockNative.trackInstall).toHaveBeenLastCalledWith(true); + }); + + it('trackInstallWithClickToken passes token and force', async () => { + mockNative.trackInstallWithClickToken.mockResolvedValue({}); + await api.LinkTrail.trackInstallWithClickToken('tok'); + expect(mockNative.trackInstallWithClickToken).toHaveBeenLastCalledWith( + 'tok', + false + ); + await api.LinkTrail.trackInstallWithClickToken('tok', true); + expect(mockNative.trackInstallWithClickToken).toHaveBeenLastCalledWith( + 'tok', + true + ); + }); + + it('trackEvent flattens value/currency into positional args', async () => { + mockNative.trackEvent.mockResolvedValue({ ok: true }); + await api.LinkTrail.trackEvent('purchase', { value: 9.99, currency: 'USD' }); + expect(mockNative.trackEvent).toHaveBeenLastCalledWith( + 'purchase', + 9.99, + 'USD' + ); + await api.LinkTrail.trackEvent('signup'); + expect(mockNative.trackEvent).toHaveBeenLastCalledWith( + 'signup', + undefined, + undefined + ); + }); + + it('rejections from native propagate to the caller', async () => { + mockNative.trackEvent.mockRejectedValue(new Error('offline')); + await expect(api.LinkTrail.trackEvent('x')).rejects.toThrow('offline'); + mockNative.trackInstall.mockRejectedValue(new Error('boom')); + await expect(api.LinkTrail.trackInstall()).rejects.toThrow('boom'); + }); + + it('getLastAttribution / getLastDeepLink resolve native values (including null)', async () => { + mockNative.getLastAttribution.mockResolvedValue(null); + mockNative.getLastDeepLink.mockResolvedValue({ path: '/z' }); + await expect(api.LinkTrail.getLastAttribution()).resolves.toBeNull(); + await expect(api.LinkTrail.getLastDeepLink()).resolves.toEqual({ + path: '/z', + }); + }); +}); + +describe('consent, SKAdNetwork and testing helpers', () => { + it('setConsent forwards the boolean', () => { + api.LinkTrail.setConsent(true); + api.LinkTrail.setConsent(false); + expect(mockNative.setConsent.mock.calls).toEqual([[true], [false]]); + }); + + it('SKAdNetwork calls are forwarded', () => { + api.LinkTrail.registerForSKAdAttribution(); + api.LinkTrail.updateConversionValue(5, 'high'); + api.LinkTrail.updateConversionValue(3); + expect(mockNative.registerForSKAdAttribution).toHaveBeenCalledTimes(1); + expect(mockNative.updateConversionValue.mock.calls).toEqual([ + [5, 'high'], + [3, undefined], + ]); + }); + + it('resetForTesting is forwarded', () => { + api.LinkTrail.resetForTesting(); + expect(mockNative.resetForTesting).toHaveBeenCalledTimes(1); + }); +}); + +describe('requestTrackingAuthorization (platform behavior)', () => { + it.each(['ios', 'android'] as const)( + 'delegates to native on %s and returns its result', + async (os) => { + Platform.OS = os; + mockNative.requestTrackingAuthorization.mockResolvedValue(true); + await expect(api.LinkTrail.requestTrackingAuthorization()).resolves.toBe( + true + ); + mockNative.requestTrackingAuthorization.mockResolvedValue(false); + await expect(api.LinkTrail.requestTrackingAuthorization()).resolves.toBe( + false + ); + } + ); +}); + +describe('LinkTrailPasteButton', () => { + const nativeComponentMock = jest.fn(() => null); + + function loadButton() { + let mod!: typeof import('../src/LinkTrailPasteButton'); + jest.isolateModules(() => { + jest.doMock('../src/LinkTrailPasteButtonNativeComponent', () => ({ + __esModule: true, + default: nativeComponentMock, + })); + mod = require('../src/LinkTrailPasteButton'); + }); + return mod.LinkTrailPasteButton; + } + + afterEach(() => { + Platform.OS = 'ios'; + }); + + it('renders nothing on Android', () => { + Platform.OS = 'android'; + expect(loadButton()({})).toBeNull(); + }); + + it('on iOS renders the native component and unwraps the token event', () => { + Platform.OS = 'ios'; + const onTokenPasted = jest.fn(); + const el = loadButton()({ + onTokenPasted, + displayMode: 'iconOnly', + }) as unknown as { + type: unknown; + props: { + displayMode: string; + onTokenPasted?: (e: { nativeEvent: { token: string } }) => void; + }; + }; + expect(el.type).toBe(nativeComponentMock); + expect(el.props.displayMode).toBe('iconOnly'); + el.props.onTokenPasted?.({ nativeEvent: { token: 'abc' } }); + expect(onTokenPasted).toHaveBeenCalledWith('abc'); + }); + + it('passes no handler when onTokenPasted is omitted', () => { + Platform.OS = 'ios'; + const el = loadButton()({}) as unknown as { + props: { onTokenPasted?: unknown }; + }; + expect(el.props.onTokenPasted).toBeUndefined(); + }); +}); diff --git a/__tests__/parity.test.ts b/__tests__/parity.test.ts new file mode 100644 index 0000000..b5b443d --- /dev/null +++ b/__tests__/parity.test.ts @@ -0,0 +1,275 @@ +/* eslint-env jest */ +// wrapper-parity 0.1.1: link/attribution buffering, error codes, version. +import { Linking } from 'react-native'; + +type Listener = (payload: unknown) => void; + +const emitters: Record<'onLink' | 'onAttribution' | 'onError', Listener[]> = { + onLink: [], + onAttribution: [], + onError: [], +}; +const emitterMock = (name: keyof typeof emitters) => + jest.fn((l: Listener) => { + emitters[name].push(l); + return { remove: jest.fn() }; + }); + +const mockNative = { + configure: jest.fn(), + trackInstall: jest.fn(), + trackInstallWithClickToken: jest.fn(), + trackEvent: jest.fn(), + handleDeepLink: jest.fn(), + getLastAttribution: jest.fn(), + getLastDeepLink: jest.fn(), + requestTrackingAuthorization: jest.fn(), + registerForSKAdAttribution: jest.fn(), + updateConversionValue: jest.fn(), + setConsent: jest.fn(), + resetForTesting: jest.fn(), + startLinkDelivery: jest.fn(), + nativeSdkVersion: jest.fn(), + onLink: emitterMock('onLink'), + onAttribution: emitterMock('onAttribution'), + onError: emitterMock('onError'), +}; + +jest.mock('../src/NativeLinkTrail', () => ({ + __esModule: true, + default: mockNative, +})); + +type Api = typeof import('../src/index'); +let api: Api; +let urlListener: ((e: { url: string }) => void) | undefined; + +const nativeEmit = (name: keyof typeof emitters, payload: unknown) => + emitters[name].forEach((l) => l(payload)); +const flush = () => new Promise((r) => setImmediate(r)); +const link = (path: string) => ({ path, hasRoutableDestination: true }); + +beforeEach(() => { + jest.clearAllMocks(); + emitters.onLink = []; + emitters.onAttribution = []; + emitters.onError = []; + mockNative.configure.mockResolvedValue(undefined); + mockNative.handleDeepLink.mockResolvedValue(null); + urlListener = undefined; + jest.spyOn(Linking, 'getInitialURL').mockResolvedValue(null); + jest.spyOn(Linking, 'addEventListener').mockImplementation((( + _t: string, + h: (e: { url: string }) => void, + ) => { + urlListener = h; + return { remove: jest.fn() }; + }) as never); + jest.isolateModules(() => { + api = require('../src/index'); + }); +}); + +describe('onLink buffering', () => { + it('registers native onLink only when the first listener subscribes', async () => { + await api.LinkTrail.configure('k'); + expect(mockNative.startLinkDelivery).not.toHaveBeenCalled(); + api.LinkTrail.onLink(jest.fn()); + api.LinkTrail.onLink(jest.fn()); + expect(mockNative.startLinkDelivery).toHaveBeenCalledTimes(1); + expect(mockNative.onLink).toHaveBeenCalledTimes(1); + }); + + it('subscribes the JS emitter before asking native to deliver', () => { + const order: string[] = []; + mockNative.onLink.mockImplementationOnce((l: Listener) => { + order.push('subscribe'); + emitters.onLink.push(l); + return { remove: jest.fn() }; + }); + mockNative.startLinkDelivery.mockImplementationOnce(() => { + order.push('start'); + nativeEmit('onLink', { link: link('/saved'), source: 'deferred' }); + }); + const listener = jest.fn(); + api.LinkTrail.onLink(listener); + expect(order).toEqual(['subscribe', 'start']); + expect(listener).toHaveBeenCalledWith(link('/saved'), 'deferred'); + }); + + it('buffers links resolved before any listener and delivers them once, in order', async () => { + mockNative.handleDeepLink.mockImplementation(async (url: string) => + link(new URL(url).pathname), + ); + (Linking.getInitialURL as jest.Mock).mockResolvedValue( + 'https://x.linktrail.io/a', + ); + await api.LinkTrail.configure('k'); + await flush(); + urlListener?.({ url: 'https://x.linktrail.io/b' }); + await flush(); + + const first = jest.fn(); + api.LinkTrail.onLink(first); + expect(first.mock.calls).toEqual([ + [link('/a'), 'reengagement'], + [link('/b'), 'reengagement'], + ]); + + const second = jest.fn(); + api.LinkTrail.onLink(second); + expect(second).not.toHaveBeenCalled(); // no replays for later listeners + + // After delivery started, links come from native only (no double delivery). + urlListener?.({ url: 'https://x.linktrail.io/c' }); + await flush(); + expect(first).toHaveBeenCalledTimes(2); + nativeEmit('onLink', { link: link('/c'), source: 'reengagement' }); + expect(first).toHaveBeenCalledTimes(3); + expect(second).toHaveBeenCalledWith(link('/c'), 'reengagement'); + }); + + it('caps the buffer at 10, dropping the oldest', async () => { + mockNative.handleDeepLink.mockImplementation(async (url: string) => + link(new URL(url).pathname), + ); + await api.LinkTrail.configure('k'); + for (let i = 0; i < 12; i++) + urlListener?.({ url: `https://x.linktrail.io/${i}` }); + await flush(); + const listener = jest.fn(); + api.LinkTrail.onLink(listener); + expect(listener.mock.calls.map(([l]) => l.path)).toEqual( + Array.from({ length: 10 }, (_, i) => `/${i + 2}`), + ); + }); + + it('does not buffer non-LinkTrail URLs', async () => { + await api.LinkTrail.configure('k'); + urlListener?.({ url: 'https://example.com' }); + await flush(); + const listener = jest.fn(); + api.LinkTrail.onLink(listener); + expect(listener).not.toHaveBeenCalled(); + }); + + it('remove() stops delivery to that listener only', () => { + const a = jest.fn(); + const b = jest.fn(); + const sub = api.LinkTrail.onLink(a); + api.LinkTrail.onLink(b); + sub.remove(); + nativeEmit('onLink', { link: link('/x'), source: 'reengagement' }); + expect(a).not.toHaveBeenCalled(); + expect(b).toHaveBeenCalledTimes(1); + }); +}); + +describe('onAttribution replay', () => { + it('keeps the last attribution and replays it to the first listener only', async () => { + await api.LinkTrail.configure('k'); + nativeEmit('onAttribution', { attributed: false }); + nativeEmit('onAttribution', { attributed: true, id: 7 }); + const first = jest.fn(); + api.LinkTrail.onAttribution(first); + expect(first.mock.calls).toEqual([[{ attributed: true, id: 7 }]]); + const second = jest.fn(); + api.LinkTrail.onAttribution(second); + expect(second).not.toHaveBeenCalled(); + nativeEmit('onAttribution', { attributed: true, id: 8 }); + expect(first).toHaveBeenLastCalledWith({ attributed: true, id: 8 }); + expect(second).toHaveBeenCalledWith({ attributed: true, id: 8 }); + }); + + it('does not replay when the first listener already received live values', async () => { + const first = jest.fn(); + api.LinkTrail.onAttribution(first); + nativeEmit('onAttribution', { attributed: true }); + const later = jest.fn(); + api.LinkTrail.onAttribution(later); + expect(later).not.toHaveBeenCalled(); + expect(mockNative.onAttribution).toHaveBeenCalledTimes(1); + }); +}); + +describe('error codes', () => { + it('rejects with a LinkTrailError: camelCase code, legacyCode, native message', async () => { + mockNative.trackEvent.mockRejectedValue( + Object.assign(new Error('Server returned status 500: boom'), { + code: 'server', + userInfo: { statusCode: 500, body: 'boom' }, + }), + ); + const err = await api.LinkTrail.trackEvent('x').catch((e) => e); + expect(err).toBeInstanceOf(api.LinkTrailError); + expect(err).toMatchObject({ + code: 'server', + legacyCode: 'server', + message: 'Server returned status 500: boom', + statusCode: 500, + body: 'boom', + }); + }); + + it.each([ + ['missingApiKey', 'missing_api_key'], + ['invalidApiKey', 'invalid_api_key'], + ['notConfigured', 'not_configured'], + ['invalidUrl', 'invalid_url'], + ['notALinkTrailUrl', 'not_a_linktrail_url'], + ['transport', 'transport'], + ['decoding', 'decoding'], + ['emptyResponse', 'empty_response'], + ['invalidArgument', 'invalid_argument'], + ])( + 'maps %s <-> legacy %s (either form from native)', + async (code, legacy) => { + for (const nativeCode of [code, legacy]) { + mockNative.trackInstall.mockRejectedValueOnce( + Object.assign(new Error('m'), { code: nativeCode }), + ); + await expect(api.LinkTrail.trackInstall()).rejects.toMatchObject({ + code, + legacyCode: legacy, + }); + } + }, + ); + + it('maps unrecognised codes to unknown and omits statusCode/body', async () => { + mockNative.handleDeepLink.mockRejectedValue( + Object.assign(new Error('weird'), { code: 'E_WHAT' }), + ); + const err = await api.LinkTrail.handleDeepLink('https://x').catch((e) => e); + expect(err.code).toBe('unknown'); + expect(err).not.toHaveProperty('statusCode'); + expect(err).not.toHaveProperty('body'); + }); + + it('onError carries statusCode and body for server errors', () => { + const listener = jest.fn(); + api.LinkTrail.onError(listener); + nativeEmit('onError', { + code: 'server', + message: 'Server returned status 503', + statusCode: 503, + }); + expect(listener).toHaveBeenCalledWith({ + code: 'server', + legacyCode: 'server', + message: 'Server returned status 503', + statusCode: 503, + }); + }); +}); + +describe('version', () => { + it('LinkTrail.version equals package.json', () => { + expect(api.LinkTrail.version).toBe(require('../package.json').version); + }); + + it('nativeSdkVersion comes from native', () => { + mockNative.nativeSdkVersion.mockReturnValue('0.1.0'); + expect(api.LinkTrail.nativeSdkVersion).toBe('0.1.0'); + }); +}); diff --git a/android/src/main/java/io/linktrail/reactnative/LinkTrailModule.kt b/android/src/main/java/io/linktrail/reactnative/LinkTrailModule.kt index 6f4aea6..b174345 100644 --- a/android/src/main/java/io/linktrail/reactnative/LinkTrailModule.kt +++ b/android/src/main/java/io/linktrail/reactnative/LinkTrailModule.kt @@ -41,25 +41,50 @@ class LinkTrailModule(reactContext: ReactApplicationContext) : try { val sdk = LinkTrail.configure(reactApplicationContext, apiKey, parseOptions(options)) sdk.onAttribution { attribution -> emitOnAttribution(attributionToMap(attribution)) } - sdk.onLink { link, source -> - emitOnLink( - Arguments.createMap().apply { - putMap("link", deepLinkToMap(link)) - putString("source", sourceName(source)) - } - ) - } + // Native onLink is registered only once a JS listener exists (startLinkDelivery), so a + // saved deferred link stays saved natively until JS can actually receive it. + if (linkDeliveryStarted) registerOnLink(sdk) sdk.onError { error -> emitOnError( Arguments.createMap().apply { putString("code", errorCode(error)) putString("message", error.message ?: "Unknown error") + (error as? LinkTrailError.Server)?.let { + putInt("statusCode", it.statusCode) + it.body?.let { body -> putString("body", body) } + } } ) } promise.resolve(null) } catch (t: Throwable) { - promise.reject(errorCode(t), t.message, t) + reject(promise, t) + } + } + + @Volatile private var linkDeliveryStarted = false + + /** + * Called by JS when the first `onLink` listener subscribes. Registers the native onLink now (and + * on every later configure), so the native SDK hands over its saved deferred link only when there + * is a JS listener to receive it. + */ + override fun startLinkDelivery() { + linkDeliveryStarted = true + LinkTrail.shared?.let(::registerOnLink) + } + + /** The embedded native LinkTrail Android SDK version, e.g. `"0.1.0"`. */ + override fun nativeSdkVersion(): String = LinkTrail.VERSION + + private fun registerOnLink(sdk: LinkTrail) { + sdk.onLink { link, source -> + emitOnLink( + Arguments.createMap().apply { + putMap("link", deepLinkToMap(link)) + putString("source", sourceName(source)) + } + ) } } @@ -71,7 +96,7 @@ class LinkTrailModule(reactContext: ReactApplicationContext) : try { promise.resolve(attributionToMap(sdk.trackInstallAsync(force))) } catch (t: Throwable) { - promise.reject(errorCode(t), t.message, t) + reject(promise, t) } } } @@ -84,7 +109,7 @@ class LinkTrailModule(reactContext: ReactApplicationContext) : try { promise.resolve(attributionToMap(sdk.trackInstallAsync(force))) } catch (t: Throwable) { - promise.reject(errorCode(t), t.message, t) + reject(promise, t) } } } @@ -101,7 +126,7 @@ class LinkTrailModule(reactContext: ReactApplicationContext) : } ) } catch (t: Throwable) { - promise.reject(errorCode(t), t.message, t) + reject(promise, t) } } } @@ -116,7 +141,7 @@ class LinkTrailModule(reactContext: ReactApplicationContext) : } catch (e: LinkTrailError.NotALinkTrailUrl) { promise.resolve(null) } catch (t: Throwable) { - promise.reject(errorCode(t), t.message, t) + reject(promise, t) } } } @@ -131,8 +156,11 @@ class LinkTrailModule(reactContext: ReactApplicationContext) : // ── iOS-only APIs (no-ops on Android) ────────────────────────────────────── + // Android has no App Tracking Transparency: no OS gate restricts tracking, so + // report "not restricted" (true), matching the Flutter SDK. App-level consent is + // separate — see setConsent. override fun requestTrackingAuthorization(promise: Promise) { - promise.resolve(false) + promise.resolve(true) } override fun registerForSKAdAttribution() = Unit @@ -157,7 +185,7 @@ class LinkTrailModule(reactContext: ReactApplicationContext) : val sdk = LinkTrail.shared if (sdk == null) { promise.reject( - "not_configured", + "notConfigured", "LinkTrail is not configured. Call LinkTrail.configure(apiKey) first." ) } @@ -238,18 +266,33 @@ class LinkTrailModule(reactContext: ReactApplicationContext) : values.forEach { (key, value) -> putString(key, value) } } + /** camelCase codes matching the native `LinkTrailError` case names (wrapper-parity 0.1.1). */ private fun errorCode(error: Throwable): String = when (error) { - is LinkTrailError.MissingApiKey -> "missing_api_key" - is LinkTrailError.InvalidApiKey -> "invalid_api_key" + is LinkTrailError.MissingApiKey -> "missingApiKey" + is LinkTrailError.InvalidApiKey -> "invalidApiKey" is LinkTrailError.Transport -> "transport" is LinkTrailError.Server -> "server" is LinkTrailError.Decoding -> "decoding" - is LinkTrailError.EmptyResponse -> "empty_response" - is LinkTrailError.InvalidUrl -> "invalid_url" - is LinkTrailError.NotALinkTrailUrl -> "not_a_linktrail_url" + is LinkTrailError.EmptyResponse -> "emptyResponse" + is LinkTrailError.InvalidUrl -> "invalidUrl" + is LinkTrailError.NotALinkTrailUrl -> "notALinkTrailUrl" else -> "unknown" } + /** Rejects with the camelCase code, the native message unchanged, and statusCode/body for server errors. */ + private fun reject(promise: Promise, error: Throwable) { + val server = error as? LinkTrailError.Server + if (server == null) { + promise.reject(errorCode(error), error.message, error) + return + } + val userInfo = Arguments.createMap().apply { + putInt("statusCode", server.statusCode) + server.body?.let { putString("body", it) } + } + promise.reject(errorCode(error), error.message, error, userInfo) + } + private fun ReadableMap.booleanOrDefault(key: String, fallback: Boolean): Boolean = if (hasKey(key) && getType(key) == ReadableType.Boolean) getBoolean(key) else fallback diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 075a72d..5bed9df 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -37,7 +37,7 @@ PODS: - hermes-engine (0.80.3): - hermes-engine/Pre-built (= 0.80.3) - hermes-engine/Pre-built (0.80.3) - - LinktrailReactNative (0.1.0): + - LinktrailReactNative (0.1.1): - boost - DoubleConversion - fast_float @@ -2521,7 +2521,7 @@ SPEC CHECKSUMS: fmt: a40bb5bd0294ea969aaaba240a927bd33d878cdd glog: 5683914934d5b6e4240e497e0f4a3b42d1854183 hermes-engine: fff23161928673afcd95f63146e651726f68b384 - LinktrailReactNative: 6f4eccc80546fe0cd08fb17d598aac809bb500b6 + LinktrailReactNative: b3151edec47a2dfa9c70e816f43d85307d779461 LinkTrailSDK: 622127713ad7b158aae26e2c1a8ad143d74342b2 RCT-Folly: 846fda9475e61ec7bcbf8a3fe81edfcaeb090669 RCTDeprecation: 2ebc9cc0329ede460c5d52956ce5e1ab715c2c2c diff --git a/example/src/attribution.ts b/example/src/attribution.ts index c4420be..6c86296 100644 --- a/example/src/attribution.ts +++ b/example/src/attribution.ts @@ -120,7 +120,7 @@ export function startAttribution( // Surface backend failures — most importantly a rejected API key. subscriptions.push( LinkTrail.onError((error) => { - if (error.code === 'invalid_api_key') { + if (error.code === 'invalidApiKey') { console.warn( '⚠️ LinkTrail: API key rejected by the server — check your lt_live_… key.', ); diff --git a/ios/LinkTrailModule.mm b/ios/LinkTrailModule.mm index c46bce2..0ff1f5f 100644 --- a/ios/LinkTrailModule.mm +++ b/ios/LinkTrailModule.mm @@ -20,6 +20,16 @@ @interface LinkTrailModule : NativeLinkTrailSpecBase @end +// Bridges the Swift (code, message, userInfo) reject to RN's; userInfo surfaces as +// `error.userInfo` in JS (statusCode/body for server errors). +static void (^LTReject(RCTPromiseRejectBlock reject))(NSString *, NSString *, NSDictionary *) +{ + return ^(NSString *code, NSString *message, NSDictionary *userInfo) { + NSError *error = userInfo ? [NSError errorWithDomain:@"LinkTrail" code:0 userInfo:userInfo] : nil; + reject(code, message, error); + }; +} + @implementation LinkTrailModule { LinkTrailModuleImpl *_impl; } @@ -52,7 +62,7 @@ - (void)configure:(NSString *)apiKey [_impl configure:apiKey options:options resolve:resolve - reject:^(NSString *code, NSString *message) { reject(code, message, nil); }]; + reject:LTReject(reject)]; } - (void)trackInstall:(BOOL)force @@ -61,7 +71,7 @@ - (void)trackInstall:(BOOL)force { [_impl trackInstall:force resolve:resolve - reject:^(NSString *code, NSString *message) { reject(code, message, nil); }]; + reject:LTReject(reject)]; } - (void)trackEvent:(NSString *)name @@ -74,7 +84,7 @@ - (void)trackEvent:(NSString *)name value:value currency:currency resolve:resolve - reject:^(NSString *code, NSString *message) { reject(code, message, nil); }]; + reject:LTReject(reject)]; } - (void)trackInstallWithClickToken:(NSString *)clickToken @@ -85,7 +95,7 @@ - (void)trackInstallWithClickToken:(NSString *)clickToken [_impl trackInstallWithClickToken:clickToken force:force resolve:resolve - reject:^(NSString *code, NSString *message) { reject(code, message, nil); }]; + reject:LTReject(reject)]; } - (void)handleDeepLink:(NSString *)url @@ -94,7 +104,7 @@ - (void)handleDeepLink:(NSString *)url { [_impl handleDeepLink:url resolve:resolve - reject:^(NSString *code, NSString *message) { reject(code, message, nil); }]; + reject:LTReject(reject)]; } - (void)getLastAttribution:(RCTPromiseResolveBlock)resolve @@ -113,7 +123,7 @@ - (void)requestTrackingAuthorization:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { [_impl requestTrackingAuthorization:resolve - reject:^(NSString *code, NSString *message) { reject(code, message, nil); }]; + reject:LTReject(reject)]; } - (void)registerForSKAdAttribution @@ -131,6 +141,16 @@ - (void)setConsent:(BOOL)granted [_impl setConsent:granted]; } +- (void)startLinkDelivery +{ + [_impl startLinkDelivery]; +} + +- (NSString *)nativeSdkVersion +{ + return [_impl nativeSdkVersion]; +} + - (void)resetForTesting { [_impl resetForTesting]; diff --git a/ios/LinkTrailModuleImpl.swift b/ios/LinkTrailModuleImpl.swift index a9befed..0073e38 100644 --- a/ios/LinkTrailModuleImpl.swift +++ b/ios/LinkTrailModuleImpl.swift @@ -8,7 +8,8 @@ import LinkTrailSDK public final class LinkTrailModuleImpl: NSObject { public typealias Resolve = (Any?) -> Void - public typealias Reject = (String, String) -> Void + /// `(code, message, userInfo)`; `userInfo` carries `statusCode`/`body` for `server` errors. + public typealias Reject = (String, String, NSDictionary?) -> Void /// Event sinks wired up by the ObjC++ module to the codegen emitters. @objc public var onLink: ((NSDictionary) -> Void)? @@ -30,21 +31,39 @@ public final class LinkTrailModuleImpl: NSObject { sdk.onAttribution { [weak self] attribution in self?.onAttribution?(Self.attributionDict(attribution)) } - sdk.onLink { [weak self] link, source in - self?.onLink?([ - "link": Self.deepLinkDict(link), - "source": Self.sourceName(source), - ]) - } + // Native `onLink` is registered only once a JS listener exists (startLinkDelivery), so a + // saved deferred link stays saved natively until JS can actually receive it. + if linkDeliveryStarted { registerOnLink(sdk) } sdk.onError { [weak self] error in - self?.onError?([ - "code": Self.errorCode(error), - "message": error.localizedDescription, - ]) + self?.onError?(Self.errorPayload(error) as NSDictionary) } resolve(nil) } catch { - reject(Self.errorCode(error), error.localizedDescription) + Self.reject(reject, error) + } + } + + /// Called by JS when the first `onLink` listener subscribes. Registers the native `onLink` + /// now (and on every later `configure`), so the native SDK hands over its saved deferred link + /// only when there is a JS listener to receive it. + @objc public func startLinkDelivery() { + linkDeliveryStarted = true + if let sdk { registerOnLink(sdk) } + } + + /// The embedded native LinkTrail iOS SDK version, e.g. `"0.1.0"`. + @objc public func nativeSdkVersion() -> String { + LinkTrailSDK.LinkTrail.version + } + + private var linkDeliveryStarted = false + + private func registerOnLink(_ sdk: LinkTrailSDK.LinkTrail) { + sdk.onLink { [weak self] link, source in + self?.onLink?([ + "link": Self.deepLinkDict(link), + "source": Self.sourceName(source), + ]) } } @@ -60,7 +79,7 @@ public final class LinkTrailModuleImpl: NSObject { do { resolve(Self.attributionDict(try await sdk.trackInstall(force: force))) } catch { - reject(Self.errorCode(error), error.localizedDescription) + Self.reject(reject, error) } } } @@ -76,7 +95,7 @@ public final class LinkTrailModuleImpl: NSObject { do { resolve(Self.attributionDict(try await sdk.trackInstall(clickToken: clickToken, force: force))) } catch { - reject(Self.errorCode(error), error.localizedDescription) + Self.reject(reject, error) } } } @@ -96,7 +115,7 @@ public final class LinkTrailModuleImpl: NSObject { if let id = result.id { dict["id"] = id } resolve(dict) } catch { - reject(Self.errorCode(error), error.localizedDescription) + Self.reject(reject, error) } } } @@ -119,7 +138,7 @@ public final class LinkTrailModuleImpl: NSObject { } catch LinkTrailError.notALinkTrailURL { resolve(nil) } catch { - reject(Self.errorCode(error), error.localizedDescription) + Self.reject(reject, error) } } } @@ -169,7 +188,7 @@ public final class LinkTrailModuleImpl: NSObject { private func requireSDK(_ reject: Reject) -> LinkTrailSDK.LinkTrail? { guard let sdk else { - reject("not_configured", "LinkTrail is not configured. Call LinkTrail.configure(apiKey) first.") + reject("notConfigured", "LinkTrail is not configured. Call LinkTrail.configure(apiKey) first.", nil) return nil } return sdk @@ -254,18 +273,41 @@ public final class LinkTrailModuleImpl: NSObject { return dict as NSDictionary } - private static func errorCode(_ error: Error) -> String { + /// camelCase codes matching the native `LinkTrailError` case names (wrapper-parity 0.1.1). + static func errorCode(_ error: Error) -> String { guard let error = error as? LinkTrailError else { return "unknown" } switch error { - case .invalidURL: return "invalid_url" + case .invalidURL: return "invalidUrl" case .transport: return "transport" case .server: return "server" case .decoding: return "decoding" - case .emptyResponse: return "empty_response" - case .notALinkTrailURL: return "not_a_linktrail_url" - case .missingAPIKey: return "missing_api_key" - case .invalidAPIKey: return "invalid_api_key" + case .emptyResponse: return "emptyResponse" + case .notALinkTrailURL: return "notALinkTrailUrl" + case .missingAPIKey: return "missingApiKey" + case .invalidAPIKey: return "invalidApiKey" @unknown default: return "unknown" } } + + /// `code`, the native message unchanged, and `statusCode`/`body` for `server` errors. + static func errorPayload(_ error: Error) -> [String: Any] { + var payload: [String: Any] = [ + "code": errorCode(error), + "message": error.localizedDescription, + ] + payload.merge(errorUserInfo(error)) { _, new in new } + return payload + } + + static func errorUserInfo(_ error: Error) -> [String: Any] { + guard case let LinkTrailError.server(statusCode, body)? = error as? LinkTrailError else { return [:] } + var info: [String: Any] = ["statusCode": statusCode] + if let body { info["body"] = body } + return info + } + + private static func reject(_ reject: Reject, _ error: Error) { + let info = errorUserInfo(error) + reject(errorCode(error), error.localizedDescription, info.isEmpty ? nil : info as NSDictionary) + } } diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 0000000..5651fd8 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,13 @@ +module.exports = { + preset: 'react-native', + // Inline Babel config so the published build (react-native-builder-bob) is + // unaffected by a repo-level babel.config.js. + transform: { + '\\.[jt]sx?$': [ + 'babel-jest', + { presets: ['module:@react-native/babel-preset'], configFile: false }, + ], + }, + testMatch: ['/__tests__/**/*.test.{ts,tsx}'], + modulePathIgnorePatterns: ['/lib/', '/example/'], +}; diff --git a/package-lock.json b/package-lock.json index 6e66b24..ae12c7a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,22 +1,28 @@ { "name": "linktrail-react-native", - "version": "0.1.0", + "version": "0.1.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "linktrail-react-native", - "version": "0.1.0", + "version": "0.1.1", "license": "MIT", "devDependencies": { + "@react-native/babel-preset": "^0.80.3", "@react-native/eslint-config": "0.80.3", + "@types/jest": "^29.5.14", "@types/react": "^19.0.0", + "@types/react-test-renderer": "^19.3.0", + "babel-jest": "^29.7.0", "del-cli": "^6.0.0", "eslint": "^8.57.1", + "jest": "^29.7.0", "prettier": "2.8.8", "react": "19.1.0", "react-native": "^0.80.0", "react-native-builder-bob": "^0.40.6", + "react-test-renderer": "^19.1.0", "typescript": "^5.8.3" }, "peerDependencies": { @@ -575,6 +581,22 @@ "@babel/core": "^7.0.0" } }, + "node_modules/@babel/plugin-proposal-export-default-from": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-default-from/-/plugin-proposal-export-default-from-7.29.7.tgz", + "integrity": "sha512-p+G5BNXDcy3bOXplhY4HybQ1GxH3i2Tppmdm/3epyRu2VgJJZuUlZ61MqRTg582Q7ZLBdP7fePYvsumSEkMxcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-proposal-private-property-in-object": { "version": "7.21.0-placeholder-for-preset-env.2", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", @@ -643,6 +665,35 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-syntax-dynamic-import": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", + "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-export-default-from": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-default-from/-/plugin-syntax-export-default-from-7.29.7.tgz", + "integrity": "sha512-foag0BB37ROhdeIX9O8G0jX7hw0UekJc04cHMrYLOnrErsnBKqJGHJ8eDRpoCFZBvEPPygmmtw4qyU97qa4oOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-syntax-flow": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.29.7.tgz", @@ -1604,6 +1655,38 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-transform-react-pure-annotations": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.29.7.tgz", @@ -1670,6 +1753,51 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.29.7.tgz", + "integrity": "sha512-xmAscdE/AsqRW7vutbPNoUmu/nF5SrLKPs7aoJgEjo35lLKA/Bc0i2rMv/hr1+Y0o1bQCiVtith3u2vdgRL39Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "babel-plugin-polyfill-corejs2": "^0.4.14", + "babel-plugin-polyfill-corejs3": "^0.13.0", + "babel-plugin-polyfill-regenerator": "^0.6.5", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-runtime/node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.13.0.tgz", + "integrity": "sha512-U+GNwMdSFgzVmfhNm8GJUX88AadB3uo9KpJqS3FaqNIPKgySuvMb+bHPsOmmuWyIcuqZj/pzt1RUIUZns4y2+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.5", + "core-js-compat": "^3.43.0" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/plugin-transform-runtime/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/@babel/plugin-transform-shorthand-properties": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.29.7.tgz", @@ -2084,6 +2212,13 @@ "node": ">=6.9.0" } }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.10.1", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", @@ -2405,6 +2540,92 @@ "node": ">=8" } }, + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/console/node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/core/node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/@jest/create-cache-key-function": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/create-cache-key-function/-/create-cache-key-function-29.7.0.tgz", @@ -2434,6 +2655,33 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/@jest/fake-timers": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", @@ -2452,47 +2700,84 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/@jest/schemas": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", - "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", "dev": true, "license": "MIT", "dependencies": { - "@sinclair/typebox": "^0.27.8" + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/@jest/transform": { + "node_modules/@jest/reporters": { "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", - "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/core": "^7.11.6", + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", "@jest/types": "^29.6.3", "@jridgewell/trace-mapping": "^0.3.18", - "babel-plugin-istanbul": "^6.1.1", + "@types/node": "*", "chalk": "^4.0.0", - "convert-source-map": "^2.0.0", - "fast-json-stable-stringify": "^2.1.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "jest-regex-util": "^29.6.3", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", "jest-util": "^29.7.0", - "micromatch": "^4.0.4", - "pirates": "^4.0.4", + "jest-worker": "^29.7.0", "slash": "^3.0.0", - "write-file-atomic": "^4.0.2" + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } } }, - "node_modules/@jest/transform/node_modules/slash": { + "node_modules/@jest/reporters/node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@jest/reporters/node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", @@ -2502,44 +2787,161 @@ "node": ">=8" } }, - "node_modules/@jest/types": { + "node_modules/@jest/schemas": { "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", - "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", "dev": true, "license": "MIT", "dependencies": { - "@jest/schemas": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" + "@sinclair/typebox": "^0.27.8" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/@jridgewell/remapping": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "node_modules/@jest/source-map/node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "dev": true, "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" + "engines": { + "node": ">=6" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer/node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform/node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" } }, "node_modules/@jridgewell/resolve-uri": { @@ -2650,6 +3052,80 @@ "node": ">=18" } }, + "node_modules/@react-native/babel-plugin-codegen": { + "version": "0.80.3", + "resolved": "https://registry.npmjs.org/@react-native/babel-plugin-codegen/-/babel-plugin-codegen-0.80.3.tgz", + "integrity": "sha512-Ys4lC5DoobBWYDPwOZmyNFudddRgMaYMnt3bSxK++Z+mOuIxGxC8ue3Iupy09x0KEEAejpkaQYBoVMpO5YwUKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.25.3", + "@react-native/codegen": "0.80.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-native/babel-preset": { + "version": "0.80.3", + "resolved": "https://registry.npmjs.org/@react-native/babel-preset/-/babel-preset-0.80.3.tgz", + "integrity": "sha512-ROSWmuabB+8vho57Lrc/nFhU82Sd9/I5RI1OslE+JCnOUMdt6kz2JKd6hA7GV3UsJbkbYRXOF9KfjzX804JweQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.25.2", + "@babel/plugin-proposal-export-default-from": "^7.24.7", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-syntax-export-default-from": "^7.24.7", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-transform-arrow-functions": "^7.24.7", + "@babel/plugin-transform-async-generator-functions": "^7.25.4", + "@babel/plugin-transform-async-to-generator": "^7.24.7", + "@babel/plugin-transform-block-scoping": "^7.25.0", + "@babel/plugin-transform-class-properties": "^7.25.4", + "@babel/plugin-transform-classes": "^7.25.4", + "@babel/plugin-transform-computed-properties": "^7.24.7", + "@babel/plugin-transform-destructuring": "^7.24.8", + "@babel/plugin-transform-flow-strip-types": "^7.25.2", + "@babel/plugin-transform-for-of": "^7.24.7", + "@babel/plugin-transform-function-name": "^7.25.1", + "@babel/plugin-transform-literals": "^7.25.2", + "@babel/plugin-transform-logical-assignment-operators": "^7.24.7", + "@babel/plugin-transform-modules-commonjs": "^7.24.8", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.24.7", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.7", + "@babel/plugin-transform-numeric-separator": "^7.24.7", + "@babel/plugin-transform-object-rest-spread": "^7.24.7", + "@babel/plugin-transform-optional-catch-binding": "^7.24.7", + "@babel/plugin-transform-optional-chaining": "^7.24.8", + "@babel/plugin-transform-parameters": "^7.24.7", + "@babel/plugin-transform-private-methods": "^7.24.7", + "@babel/plugin-transform-private-property-in-object": "^7.24.7", + "@babel/plugin-transform-react-display-name": "^7.24.7", + "@babel/plugin-transform-react-jsx": "^7.25.2", + "@babel/plugin-transform-react-jsx-self": "^7.24.7", + "@babel/plugin-transform-react-jsx-source": "^7.24.7", + "@babel/plugin-transform-regenerator": "^7.24.7", + "@babel/plugin-transform-runtime": "^7.24.7", + "@babel/plugin-transform-shorthand-properties": "^7.24.7", + "@babel/plugin-transform-spread": "^7.24.7", + "@babel/plugin-transform-sticky-regex": "^7.24.7", + "@babel/plugin-transform-typescript": "^7.25.2", + "@babel/plugin-transform-unicode-regex": "^7.24.7", + "@babel/template": "^7.25.0", + "@react-native/babel-plugin-codegen": "0.80.3", + "babel-plugin-syntax-hermes-parser": "0.28.1", + "babel-plugin-transform-flow-enums": "^0.0.2", + "react-refresh": "^0.14.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@babel/core": "*" + } + }, "node_modules/@react-native/codegen": { "version": "0.80.3", "resolved": "https://registry.npmjs.org/@react-native/codegen/-/codegen-0.80.3.tgz", @@ -2920,6 +3396,17 @@ "@types/istanbul-lib-report": "*" } }, + "node_modules/@types/jest": { + "version": "29.5.14", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz", + "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -2947,6 +3434,16 @@ "csstype": "^3.2.2" } }, + "node_modules/@types/react-test-renderer": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@types/react-test-renderer/-/react-test-renderer-19.3.0.tgz", + "integrity": "sha512-TX4p2D4Z2oigM3SXU7+QyLSUnSR0vR1S1Pfxm/qFL9nHQ3jclgi6ioGoWQmlIr1DeKRVmPJrmNlhgM0JlWlLUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/semver": { "version": "7.7.1", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz", @@ -3366,6 +3863,35 @@ "dev": true, "license": "MIT" }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -3753,6 +4279,16 @@ "hermes-parser": "0.28.1" } }, + "node_modules/babel-plugin-transform-flow-enums": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-flow-enums/-/babel-plugin-transform-flow-enums-0.0.2.tgz", + "integrity": "sha512-g4aaCrDDOsWjbm0PUUeVnkcVd6AKJsVc/MbnPhEotEpkeJQP6b8nzewohQi7+QS8UyPehOhGWn0nOwjvWpmMvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-flow": "^7.12.1" + } + }, "node_modules/babel-preset-current-node-syntax": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", @@ -4047,6 +4583,16 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/chrome-launcher": { "version": "0.15.2", "resolved": "https://registry.npmjs.org/chrome-launcher/-/chrome-launcher-0.15.2.tgz", @@ -4097,6 +4643,13 @@ "node": ">=8" } }, + "node_modules/cjs-module-lexer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", + "dev": true, + "license": "MIT" + }, "node_modules/clean-stack": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", @@ -4122,6 +4675,24 @@ "node": ">=12" } }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz", + "integrity": "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==", + "dev": true, + "license": "MIT" + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -4229,6 +4800,28 @@ "node": ">=4" } }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -4337,6 +4930,16 @@ "dev": true, "license": "MIT" }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/define-data-property": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", @@ -4437,6 +5040,26 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -4509,6 +5132,19 @@ "dev": true, "license": "ISC" }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -5538,6 +6174,32 @@ "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/exponential-backoff": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", @@ -6198,7 +6860,14 @@ "hermes-estree": "0.28.1" } }, - "node_modules/http-errors": { + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-errors": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", @@ -6303,6 +6972,26 @@ "node": ">=4" } }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -6601,6 +7290,16 @@ "node": ">=8" } }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/is-generator-function": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", @@ -6996,116 +7695,735 @@ "semver": "bin/semver.js" } }, - "node_modules/iterator.prototype": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", - "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/iterator.prototype": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", + "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "get-proto": "^1.0.0", + "has-symbols": "^1.1.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-changed-files/node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/jest-changed-files/node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-changed-files/node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/jest-changed-files/node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus/node_modules/dedent": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz", + "integrity": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/jest-circus/node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-circus/node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-config/node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-config/node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util/node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", "dev": true, "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.6", - "get-proto": "^1.0.0", - "has-symbols": "^1.1.0", - "set-function-name": "^2.0.2" - }, "engines": { - "node": ">= 0.4" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", "dev": true, - "license": "BlueOak-1.0.0", + "license": "MIT", "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-environment-node": { + "node_modules/jest-resolve-dependencies": { "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", - "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", "dev": true, "license": "MIT", "dependencies": { - "@jest/environment": "^29.7.0", - "@jest/fake-timers": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "jest-mock": "^29.7.0", - "jest-util": "^29.7.0" + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-get-type": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", - "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "node_modules/jest-resolve/node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "dev": true, "license": "MIT", "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=8" } }, - "node_modules/jest-haste-map": { + "node_modules/jest-runner": { "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", - "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", "dev": true, "license": "MIT", "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", "@jest/types": "^29.6.3", - "@types/graceful-fs": "^4.1.3", "@types/node": "*", - "anymatch": "^3.0.3", - "fb-watchman": "^2.0.0", + "chalk": "^4.0.0", + "emittery": "^0.13.1", "graceful-fs": "^4.2.9", - "jest-regex-util": "^29.6.3", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", "jest-worker": "^29.7.0", - "micromatch": "^4.0.4", - "walker": "^1.0.8" + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner/node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" }, - "optionalDependencies": { - "fsevents": "^2.3.2" + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/jest-message-util": { + "node_modules/jest-runner/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jest-runner/node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/jest-runtime": { "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", - "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.12.13", + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", "@jest/types": "^29.6.3", - "@types/stack-utils": "^2.0.0", + "@types/node": "*", "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", "graceful-fs": "^4.2.9", - "micromatch": "^4.0.4", - "pretty-format": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", "slash": "^3.0.0", - "stack-utils": "^2.0.3" + "strip-bom": "^4.0.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-message-util/node_modules/slash": { + "node_modules/jest-runtime/node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", @@ -7115,31 +8433,38 @@ "node": ">=8" } }, - "node_modules/jest-mock": { + "node_modules/jest-snapshot": { "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", - "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", "dev": true, "license": "MIT", "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", "@jest/types": "^29.6.3", - "@types/node": "*", - "jest-util": "^29.7.0" + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-regex-util": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", - "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, "node_modules/jest-util": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", @@ -7189,6 +8514,26 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/jest-worker": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", @@ -7276,6 +8621,13 @@ "dev": true, "license": "MIT" }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -7404,6 +8756,13 @@ "dev": true, "license": "MIT" }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, "node_modules/locate-path": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", @@ -7468,6 +8827,22 @@ "yallist": "^3.0.2" } }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/makeerror": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", @@ -8498,6 +9873,19 @@ "node": ">= 6" } }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -8649,6 +10037,23 @@ "node": ">=6" } }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, "node_modules/queue": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz", @@ -9042,6 +10447,27 @@ "node": ">=0.10.0" } }, + "node_modules/react-test-renderer": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-19.1.0.tgz", + "integrity": "sha512-jXkSl3CpvPYEF+p/eGDLB4sPoDX8pKkYvRl9+rR8HxLY0X04vW7hCm1/0zHoUSjPZ3bDa+wXWNTDVIw/R8aDVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "react-is": "^19.1.0", + "scheduler": "^0.26.0" + }, + "peerDependencies": { + "react": "^19.1.0" + } + }, + "node_modules/react-test-renderer/node_modules/react-is": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.3.0.tgz", + "integrity": "sha512-UpMYezM4v5/18F28aC66AEsjXIgE02kyEMH6yLdgLXu/UTfa1Ntwck/nNLrbqJsEXW7gPb0coNO9FQse9WTovA==", + "dev": true, + "license": "MIT" + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -9183,6 +10609,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/resolve-from": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", @@ -9193,6 +10632,16 @@ "node": ">=8" } }, + "node_modules/resolve.exports": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", + "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", @@ -9731,6 +11180,20 @@ "node": ">= 0.4" } }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/string-natural-compare": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/string-natural-compare/-/string-natural-compare-3.0.1.tgz", @@ -9895,6 +11358,16 @@ "node": ">=8" } }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/strip-final-newline": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", @@ -10354,6 +11827,21 @@ "node": ">= 0.4.0" } }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, "node_modules/vlq": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/vlq/-/vlq-1.0.1.tgz", diff --git a/package.json b/package.json index ef5bd63..9211d4a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "linktrail-react-native", - "version": "0.1.0", + "version": "0.1.1", "description": "React Native SDK for LinkTrail — mobile attribution and deferred deep linking. A New Architecture TurboModule wrapping the native LinkTrail iOS and Android SDKs.", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", @@ -24,6 +24,7 @@ ], "scripts": { "typecheck": "tsc", + "test": "jest", "lint": "eslint \"src/**/*.{ts,tsx}\"", "clean": "del-cli lib", "prepare": "bob build" @@ -52,14 +53,20 @@ "registry": "https://registry.npmjs.org/" }, "devDependencies": { + "@react-native/babel-preset": "^0.80.3", "@react-native/eslint-config": "0.80.3", + "@types/jest": "^29.5.14", "@types/react": "^19.0.0", + "@types/react-test-renderer": "^19.3.0", + "babel-jest": "^29.7.0", "del-cli": "^6.0.0", "eslint": "^8.57.1", + "jest": "^29.7.0", "prettier": "2.8.8", "react": "19.1.0", "react-native": "^0.80.0", "react-native-builder-bob": "^0.40.6", + "react-test-renderer": "^19.1.0", "typescript": "^5.8.3" }, "peerDependencies": { diff --git a/src/NativeLinkTrail.ts b/src/NativeLinkTrail.ts index 8c72ed1..9536d05 100644 --- a/src/NativeLinkTrail.ts +++ b/src/NativeLinkTrail.ts @@ -16,7 +16,7 @@ import type { export interface Spec extends TurboModule { /** * Configures the native SDK. Resolves once configured; rejects with - * `missing_api_key` when the key is blank. + * `missingApiKey` when the key is blank. */ configure(apiKey: string, options?: UnsafeObject): Promise; @@ -44,7 +44,7 @@ export interface Spec extends TurboModule { /** The most recent resolved deep link from the current session, or null. */ getLastDeepLink(): Promise; - /** iOS: requests ATT authorization. Android: resolves `false`. */ + /** iOS: requests ATT authorization. Android: no ATT, resolves `true` (not restricted). */ requestTrackingAuthorization(): Promise; /** iOS: registers for SKAdNetwork attribution. Android: no-op. */ @@ -56,6 +56,16 @@ export interface Spec extends TurboModule { /** Grants/revokes tracking consent (only meaningful with `requireConsent`). */ setConsent(granted: boolean): void; + /** + * Registers the native `onLink` callback. Called by the JS layer when the first + * `onLink` listener subscribes, so the native SDK keeps a saved deferred link + * until JS can receive it. + */ + startLinkDelivery(): void; + + /** The embedded native LinkTrail SDK version, e.g. `"0.1.0"`. */ + nativeSdkVersion(): string; + /** Clears the install flag, cached attribution, queued events, device id. */ resetForTesting(): void; diff --git a/src/errors.ts b/src/errors.ts new file mode 100644 index 0000000..9831236 --- /dev/null +++ b/src/errors.ts @@ -0,0 +1,118 @@ +/** + * Error codes, camelCase, matching the native `LinkTrailError` case names. + * `unknown` covers anything that isn't a LinkTrail error. + */ +export type LinkTrailErrorCode = + | 'missingApiKey' + | 'invalidApiKey' + | 'notConfigured' + | 'invalidUrl' + | 'notALinkTrailUrl' + | 'transport' + | 'server' + | 'decoding' + | 'emptyResponse' + | 'invalidArgument' + | 'unknown'; + +/** The snake_case codes used up to 0.1.0. Exposed as `legacyCode` for one release. */ +export type LinkTrailLegacyErrorCode = + | 'missing_api_key' + | 'invalid_api_key' + | 'not_configured' + | 'invalid_url' + | 'not_a_linktrail_url' + | 'transport' + | 'server' + | 'decoding' + | 'empty_response' + | 'invalid_argument' + | 'unknown'; + +const LEGACY: Record = { + missingApiKey: 'missing_api_key', + invalidApiKey: 'invalid_api_key', + notConfigured: 'not_configured', + invalidUrl: 'invalid_url', + notALinkTrailUrl: 'not_a_linktrail_url', + transport: 'transport', + server: 'server', + decoding: 'decoding', + emptyResponse: 'empty_response', + invalidArgument: 'invalid_argument', + unknown: 'unknown', +}; + +const FROM_LEGACY = Object.fromEntries( + Object.entries(LEGACY).map(([code, legacy]) => [legacy, code]), +) as Record; + +/** Maps a native code (camelCase, or snake_case from an older native build) to camelCase. */ +export function normalizeErrorCode(raw: unknown): LinkTrailErrorCode { + if (typeof raw !== 'string') return 'unknown'; + if (raw in LEGACY) return raw as LinkTrailErrorCode; + return FROM_LEGACY[raw] ?? 'unknown'; +} + +/** The fields shared by rejected promises and `onError` events. */ +export interface LinkTrailErrorInfo { + code: LinkTrailErrorCode; + /** + * The pre-0.1.1 snake_case code (`missing_api_key`, …). + * @deprecated Use `code`. Removed in the next minor release. + */ + legacyCode: LinkTrailLegacyErrorCode; + /** The native error's message, unchanged. */ + message: string; + /** HTTP status, for `server` errors. */ + statusCode?: number; + /** Response body, for `server` errors when the server sent one. */ + body?: string; +} + +/** The error every LinkTrail promise rejects with. */ +export class LinkTrailError extends Error implements LinkTrailErrorInfo { + readonly code: LinkTrailErrorCode; + readonly legacyCode: LinkTrailLegacyErrorCode; + readonly statusCode?: number; + readonly body?: string; + + constructor(info: Omit) { + super(info.message); + this.name = 'LinkTrailError'; + this.code = info.code; + this.legacyCode = LEGACY[info.code]; + if (info.statusCode !== undefined) this.statusCode = info.statusCode; + if (info.body !== undefined) this.body = info.body; + } +} + +type RawError = { + code?: unknown; + message?: unknown; + statusCode?: unknown; + body?: unknown; + userInfo?: { statusCode?: unknown; body?: unknown } | null; +}; + +/** Builds the error fields from a native rejection or `onError` payload. */ +export function toErrorInfo(raw: unknown): LinkTrailErrorInfo { + const e = (raw ?? {}) as RawError; + const code = normalizeErrorCode(e.code); + const statusCode = e.statusCode ?? e.userInfo?.statusCode; + const body = e.body ?? e.userInfo?.body; + const info: LinkTrailErrorInfo = { + code, + legacyCode: LEGACY[code], + message: typeof e.message === 'string' ? e.message : String(raw), + }; + if (typeof statusCode === 'number') info.statusCode = statusCode; + if (typeof body === 'string') info.body = body; + return info; +} + +/** Converts a native rejection into a `LinkTrailError`. */ +export function toLinkTrailError(raw: unknown): LinkTrailError { + if (raw instanceof LinkTrailError) return raw; + return new LinkTrailError(toErrorInfo(raw)); +} diff --git a/src/index.ts b/src/index.ts index 6d21014..54c9e9e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,12 +11,76 @@ import type { LinkTrailSubscription, } from './types'; +import { toErrorInfo, toLinkTrailError } from './errors'; +import { LINKTRAIL_RN_VERSION } from './version'; + export * from './types'; +export { + LinkTrailError, + type LinkTrailErrorCode, + type LinkTrailErrorInfo, + type LinkTrailLegacyErrorCode, +} from './errors'; export { LinkTrailPasteButton, type LinkTrailPasteButtonProps, } from './LinkTrailPasteButton'; +/** Rejects with a `LinkTrailError` (camelCase `code`, `legacyCode`, native message). */ +function call(promise: Promise): Promise { + return promise.then( + (value) => value as T, + (error: unknown) => { + throw toLinkTrailError(error); + } + ); +} + +// ── Link / attribution delivery ───────────────────────────────────────────── +// +// Native can emit before any JS listener exists (a deferred link resolving +// during the auto-tracked install, a link opened at launch). RN event emitters +// drop events with no subscriber, so the bridge owns one native subscription +// per event and fans out to JS listeners: +// - Links: the native `onLink` is registered only when the first JS listener +// subscribes (`startLinkDelivery`), so the native SDK keeps a saved deferred +// link (persisted across launches) until JS can take it. Re-engagement links +// resolved by `autoHandleLinks` before then are buffered here (max 10, oldest +// dropped) and delivered once, in order, to the first listener. +// - Attribution: the last value is kept and replayed to the first listener. + +type LinkListener = (link: LinkTrailDeepLink, source: LinkTrailLinkSource) => void; +type AttributionListener = (attribution: LinkTrailAttribution) => void; + +const LINK_BUFFER_LIMIT = 10; +const linkListeners = new Set(); +const attributionListeners = new Set(); +let linkBuffer: Array<{ link: LinkTrailDeepLink; source: LinkTrailLinkSource }> = []; +let linkDeliveryStarted = false; +let nativeLinkSubscription: LinkTrailSubscription | null = null; +let nativeAttributionSubscription: LinkTrailSubscription | null = null; +let lastAttribution: LinkTrailAttribution | null = null; +let attributionReplayed = false; + +function emitLink(link: LinkTrailDeepLink, source: LinkTrailLinkSource): void { + for (const listener of [...linkListeners]) listener(link, source); +} + +function bufferLink(link: LinkTrailDeepLink, source: LinkTrailLinkSource): void { + linkBuffer.push({ link, source }); + if (linkBuffer.length > LINK_BUFFER_LIMIT) linkBuffer.shift(); +} + +function ensureAttributionSubscription(): void { + nativeAttributionSubscription ??= NativeLinkTrail.onAttribution((payload) => { + const attribution = payload as unknown as LinkTrailAttribution; + lastAttribution = attribution; + if (attributionListeners.size === 0) return; // kept for the first listener + attributionReplayed = true; + for (const listener of [...attributionListeners]) listener(attribution); + }); +} + let linkingSubscription: LinkTrailSubscription | null = null; let initialUrlForwarded = false; @@ -25,28 +89,48 @@ function startLinkForwarding(): void { if (!initialUrlForwarded) { initialUrlForwarded = true; Linking.getInitialURL() - .then((url) => (url ? NativeLinkTrail.handleDeepLink(url) : null)) + .then((url) => (url ? forwardUrl(url) : null)) .catch(() => { // Non-LinkTrail URLs and transport failures are reported via onError. }); } linkingSubscription ??= Linking.addEventListener('url', ({ url }) => { - NativeLinkTrail.handleDeepLink(url).catch(() => {}); + forwardUrl(url).catch(() => {}); }); } +/** + * Hands a URL to native. Before the first `onLink` listener the native `onLink` + * isn't registered yet, so a resolved re-engagement link is buffered here. + */ +async function forwardUrl(url: string): Promise { + const link = (await NativeLinkTrail.handleDeepLink(url)) as LinkTrailDeepLink | null; + if (link && !linkDeliveryStarted) bufferLink(link, 'reengagement'); +} + function stopLinkForwarding(): void { linkingSubscription?.remove(); linkingSubscription = null; } export const LinkTrail = { + /** This package's version (`linktrail-react-native`), e.g. `"0.1.1"`. */ + version: LINKTRAIL_RN_VERSION, + + /** + * The embedded native LinkTrail SDK version (iOS `LinkTrailSDK` or Android + * `io.linktrail:sdk`), e.g. `"0.1.0"`. + */ + get nativeSdkVersion(): string { + return NativeLinkTrail.nativeSdkVersion(); + }, + /** * Configures the SDK with your workspace API key (`lt_live_…`). Call once at * app launch, before any other LinkTrail API. The install is tracked * automatically unless `options.autoTrackInstall` is `false`. * - * Rejects with code `missing_api_key` when the key is blank. Calling it again + * Rejects with code `missingApiKey` when the key is blank. Calling it again * with the same key and options is a no-op; different options replace the * native instance. * @@ -55,7 +139,8 @@ export const LinkTrail = { */ async configure(apiKey: string, options: LinkTrailOptions = {}): Promise { const { autoHandleLinks = true, ...nativeOptions } = options; - await NativeLinkTrail.configure(apiKey, nativeOptions); + ensureAttributionSubscription(); + await call(NativeLinkTrail.configure(apiKey, nativeOptions)); if (autoHandleLinks) { startLinkForwarding(); } else { @@ -64,33 +149,45 @@ export const LinkTrail = { }, /** - * Registers a single listener for **both** deferred deep links (first launch - * after install) and re-engagement links (app already installed) — `source` - * says which. Fires immediately if a deferred link already resolved this - * session, so registration order vs. the auto-tracked install doesn't matter. + * Registers a listener for **both** deferred deep links (first launch after + * install) and re-engagement links (app already installed) — `source` says + * which. Links that arrive before the first listener aren't lost: the first + * listener gets them once, in order (later listeners get no replays), so it's + * fine to subscribe after a splash or login screen. */ - onLink( - listener: (link: LinkTrailDeepLink, source: LinkTrailLinkSource) => void - ): LinkTrailSubscription { - return NativeLinkTrail.onLink((payload) => { - const event = payload as { - link: LinkTrailDeepLink; - source: LinkTrailLinkSource; - }; - listener(event.link, event.source); - }); + onLink(listener: LinkListener): LinkTrailSubscription { + const first = !linkDeliveryStarted; + linkListeners.add(listener); + if (first) { + linkDeliveryStarted = true; + nativeLinkSubscription ??= NativeLinkTrail.onLink((payload) => { + const event = payload as { + link: LinkTrailDeepLink; + source: LinkTrailLinkSource; + }; + emitLink(event.link, event.source); + }); + const buffered = linkBuffer; + linkBuffer = []; + for (const { link, source } of buffered) listener(link, source); + NativeLinkTrail.startLinkDelivery(); + } + return { remove: () => linkListeners.delete(listener) }; }, /** - * Registers a listener for attribution results (attributed or organic). - * Fires immediately with the cached result if one already exists. + * Registers a listener for attribution results (attributed or organic). The + * first listener also gets the latest result that arrived before it + * subscribed. */ - onAttribution( - listener: (attribution: LinkTrailAttribution) => void - ): LinkTrailSubscription { - return NativeLinkTrail.onAttribution((payload) => { - listener(payload as unknown as LinkTrailAttribution); - }); + onAttribution(listener: AttributionListener): LinkTrailSubscription { + ensureAttributionSubscription(); + attributionListeners.add(listener); + if (!attributionReplayed && lastAttribution) { + attributionReplayed = true; + listener(lastAttribution); + } + return { remove: () => attributionListeners.delete(listener) }; }, /** @@ -101,7 +198,7 @@ export const LinkTrail = { listener: (error: LinkTrailErrorEvent) => void ): LinkTrailSubscription { return NativeLinkTrail.onError((payload) => { - listener(payload as unknown as LinkTrailErrorEvent); + listener(toErrorInfo(payload)); }); }, @@ -112,7 +209,7 @@ export const LinkTrail = { * cache unless `force` is `true`. */ trackInstall(force = false): Promise { - return NativeLinkTrail.trackInstall(force) as Promise; + return call(NativeLinkTrail.trackInstall(force)); }, /** @@ -124,10 +221,7 @@ export const LinkTrail = { clickToken: string, force = false ): Promise { - return NativeLinkTrail.trackInstallWithClickToken( - clickToken, - force - ) as Promise; + return call(NativeLinkTrail.trackInstallWithClickToken(clickToken, force)); }, /** @@ -139,11 +233,9 @@ export const LinkTrail = { name: string, options: { value?: number; currency?: string } = {} ): Promise { - return NativeLinkTrail.trackEvent( - name, - options.value, - options.currency - ) as Promise; + return call( + NativeLinkTrail.trackEvent(name, options.value, options.currency) + ); }, /** @@ -154,28 +246,28 @@ export const LinkTrail = { * `autoHandleLinks` is `false`; routing normally happens via `onLink`. */ handleDeepLink(url: string): Promise { - return NativeLinkTrail.handleDeepLink( - url - ) as Promise; + return call(NativeLinkTrail.handleDeepLink(url)); }, /** The most recent attribution result (cached across launches), or `null`. */ getLastAttribution(): Promise { - return NativeLinkTrail.getLastAttribution() as Promise; + return call(NativeLinkTrail.getLastAttribution()); }, /** The most recent resolved deep link from the current session, or `null`. */ getLastDeepLink(): Promise { - return NativeLinkTrail.getLastDeepLink() as Promise; + return call(NativeLinkTrail.getLastDeepLink()); }, /** * iOS: requests App Tracking Transparency authorization (for apps using * SKAdNetwork) and resolves with whether tracking was granted. - * Android: resolves `false`. + * Android: not applicable (there is no ATT equivalent) — resolves `true`, + * meaning the platform does not restrict tracking. This is not the user's + * consent: use `setConsent` / `requireConsent` for that. */ requestTrackingAuthorization(): Promise { - return NativeLinkTrail.requestTrackingAuthorization(); + return call(NativeLinkTrail.requestTrackingAuthorization()); }, /** diff --git a/src/types.ts b/src/types.ts index 4ba0702..c779a17 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,3 +1,5 @@ +import type { LinkTrailErrorInfo } from './errors'; + /** * The deep-link payload the SDK routes on. Returned by install attribution * (deferred deep linking), re-engagement opens, and link resolution. @@ -48,17 +50,12 @@ export interface LinkTrailEventResult { attributed: boolean; } -/** An error reported by the native SDK via the `onError` callback. */ -export interface LinkTrailErrorEvent { - /** - * Stable machine-readable code: `missing_api_key`, `invalid_api_key`, - * `transport`, `server`, `decoding`, `empty_response`, `invalid_url`, - * `not_a_linktrail_url`, `not_configured`, or `unknown`. - */ - code: string; - /** Human-readable description. */ - message: string; -} +/** + * An error reported by the native SDK via the `onError` callback: `code` + * (camelCase), `legacyCode` (old snake_case, deprecated), the native `message`, + * and `statusCode`/`body` for `server` errors. + */ +export type LinkTrailErrorEvent = LinkTrailErrorInfo; /** Severity levels for SDK logging, from most to least verbose. */ export type LinkTrailLogLevel = 'debug' | 'info' | 'warning' | 'error' | 'none'; diff --git a/src/version.ts b/src/version.ts new file mode 100644 index 0000000..b524ce7 --- /dev/null +++ b/src/version.ts @@ -0,0 +1,2 @@ +/** The linktrail-react-native package version. Kept equal to package.json by a test. */ +export const LINKTRAIL_RN_VERSION = '0.1.1';