@advenue/react-native 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +55 -0
- package/dist/index.d.ts +161 -0
- package/dist/index.js +563 -0
- package/package.json +68 -0
- package/plugin/index.js +40 -0
- package/plugin/links.js +79 -0
- package/plugin/package.json +3 -0
- package/src/deep-links.ts +85 -0
- package/src/index.ts +574 -0
- package/src/mmkv-storage.ts +21 -0
- package/src/native.ts +76 -0
- package/src/secure-store.ts +19 -0
package/plugin/links.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
// Pure config transforms for the Advenue Expo config plugin. Kept free of any
|
|
3
|
+
// @expo/config-plugins import so they are unit-testable with plain objects.
|
|
4
|
+
|
|
5
|
+
const ASSOC_KEY = 'com.apple.developer.associated-domains';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Returns a copy of the iOS entitlements with `applinks:<domain>` added for each
|
|
9
|
+
* domain (deduped, existing entries preserved). Enables iOS Universal Links.
|
|
10
|
+
*/
|
|
11
|
+
function addAssociatedDomains(entitlements, domains) {
|
|
12
|
+
const existing = Array.isArray(entitlements[ASSOC_KEY]) ? entitlements[ASSOC_KEY] : [];
|
|
13
|
+
const next = [...existing];
|
|
14
|
+
for (const domain of domains) {
|
|
15
|
+
const entry = `applinks:${domain}`;
|
|
16
|
+
if (!next.includes(entry)) {
|
|
17
|
+
next.push(entry);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return { ...entitlements, [ASSOC_KEY]: next };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function attr(name) {
|
|
24
|
+
return { $: { 'android:name': name } };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function findMainActivity(manifest) {
|
|
28
|
+
const application = manifest?.manifest?.application?.[0];
|
|
29
|
+
const activities = application?.activity ?? [];
|
|
30
|
+
return (
|
|
31
|
+
activities.find((activity) =>
|
|
32
|
+
(activity['intent-filter'] ?? []).some(
|
|
33
|
+
(filter) =>
|
|
34
|
+
(filter.action ?? []).some(
|
|
35
|
+
(a) => a.$?.['android:name'] === 'android.intent.action.MAIN',
|
|
36
|
+
) &&
|
|
37
|
+
(filter.category ?? []).some(
|
|
38
|
+
(c) => c.$?.['android:name'] === 'android.intent.category.LAUNCHER',
|
|
39
|
+
),
|
|
40
|
+
),
|
|
41
|
+
) ?? activities[0]
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Appends an autoVerify App Links intent filter (https + each host) to the main
|
|
47
|
+
* activity, enabling Android App Links. Mutates and returns the manifest.
|
|
48
|
+
* Idempotent: a filter already covering a host is not duplicated. No-op when
|
|
49
|
+
* `domains` is empty or no activity exists.
|
|
50
|
+
*/
|
|
51
|
+
function addAppLinksIntentFilter(manifest, domains) {
|
|
52
|
+
if (!domains || domains.length === 0) {
|
|
53
|
+
return manifest;
|
|
54
|
+
}
|
|
55
|
+
const activity = findMainActivity(manifest);
|
|
56
|
+
if (!activity) {
|
|
57
|
+
return manifest;
|
|
58
|
+
}
|
|
59
|
+
activity['intent-filter'] ??= [];
|
|
60
|
+
const filters = activity['intent-filter'];
|
|
61
|
+
|
|
62
|
+
const alreadyCovered = (host) =>
|
|
63
|
+
filters.some((filter) => (filter.data ?? []).some((d) => d.$?.['android:host'] === host));
|
|
64
|
+
const fresh = domains.filter((host) => !alreadyCovered(host));
|
|
65
|
+
if (fresh.length === 0) {
|
|
66
|
+
return manifest;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
filters.push({
|
|
70
|
+
$: { 'android:autoVerify': 'true' },
|
|
71
|
+
action: [attr('android.intent.action.VIEW')],
|
|
72
|
+
category: [attr('android.intent.category.DEFAULT'), attr('android.intent.category.BROWSABLE')],
|
|
73
|
+
data: fresh.map((host) => ({ $: { 'android:scheme': 'https', 'android:host': host } })),
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
return manifest;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
module.exports = { addAssociatedDomains, addAppLinksIntentFilter, ASSOC_KEY };
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { DeepLink } from '@advenue/sdk-core';
|
|
2
|
+
|
|
3
|
+
/** Handle for tearing down the OS deep-link listener. */
|
|
4
|
+
export interface DeepLinkSubscription {
|
|
5
|
+
remove(): void;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/** Subscription plus a `ready` promise that settles once cold-start resolution finishes. */
|
|
9
|
+
export interface DeepLinkHandle extends DeepLinkSubscription {
|
|
10
|
+
/** Resolves after the cold-start link (direct or deferred) has been delivered. */
|
|
11
|
+
ready: Promise<void>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Parses an OS-delivered link (Universal Link, App Link, or custom scheme) into
|
|
16
|
+
* the unified {@link DeepLink} shape. The `deepLinkValue` is a best-effort path
|
|
17
|
+
* (host + path for custom schemes, path for http(s)); the raw `url` is always
|
|
18
|
+
* provided so the host app can parse it itself when needed.
|
|
19
|
+
*/
|
|
20
|
+
export function parseDirectLink(url: string): DeepLink {
|
|
21
|
+
let deepLinkValue = '';
|
|
22
|
+
let params: Record<string, string> | undefined;
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const parsed = new URL(url);
|
|
26
|
+
const path = parsed.pathname.replace(/^\/+/, '');
|
|
27
|
+
// Custom schemes (myapp://product/1) put the first segment in the host.
|
|
28
|
+
const isHttp = parsed.protocol === 'http:' || parsed.protocol === 'https:';
|
|
29
|
+
deepLinkValue = isHttp ? path : [parsed.host, path].filter(Boolean).join('/');
|
|
30
|
+
|
|
31
|
+
for (const [k, v] of parsed.searchParams.entries()) {
|
|
32
|
+
params ??= {};
|
|
33
|
+
params[k] = v;
|
|
34
|
+
}
|
|
35
|
+
} catch {
|
|
36
|
+
// Unparseable URL — surface the raw value so the app can still route.
|
|
37
|
+
deepLinkValue = url;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
deepLinkValue,
|
|
42
|
+
...(params ? { params } : {}),
|
|
43
|
+
url,
|
|
44
|
+
isDeferred: false,
|
|
45
|
+
isFirstLaunch: false,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface DeepLinkSetup {
|
|
50
|
+
/** OS cold-start URL (RN Linking.getInitialURL). */
|
|
51
|
+
getInitialURL: () => Promise<string | null>;
|
|
52
|
+
/** Subscribes to warm OS links (RN Linking 'url' event). */
|
|
53
|
+
addUrlListener: (handler: (url: string) => void) => DeepLinkSubscription;
|
|
54
|
+
/** Resolves the deferred deep link (client.resolveDeferredDeepLink). */
|
|
55
|
+
resolveDeferred: () => Promise<DeepLink | null>;
|
|
56
|
+
/** Fired for every resolved deep link (direct or deferred). The app navigates. */
|
|
57
|
+
onDeepLink: (link: DeepLink) => void;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Unifies direct and deferred deep links behind a single callback (AppsFlyer UDL
|
|
62
|
+
* style). The OS-link listener is registered synchronously and the handle is
|
|
63
|
+
* returned immediately so app init never blocks on the deferred poll (which can
|
|
64
|
+
* back off for seconds). Cold-start resolution runs in the background: a direct
|
|
65
|
+
* OS link wins and deferred resolution is skipped (the user arrived via a real
|
|
66
|
+
* link); otherwise the deferred poll runs and fires only when it yields a
|
|
67
|
+
* `deepLinkValue`. Await `handle.ready` to observe completion (used in tests).
|
|
68
|
+
*/
|
|
69
|
+
export function setupDeepLinks(setup: DeepLinkSetup): DeepLinkHandle {
|
|
70
|
+
const listener = setup.addUrlListener((url) => setup.onDeepLink(parseDirectLink(url)));
|
|
71
|
+
|
|
72
|
+
const ready = (async () => {
|
|
73
|
+
const initial = await setup.getInitialURL();
|
|
74
|
+
if (initial) {
|
|
75
|
+
setup.onDeepLink(parseDirectLink(initial));
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const deferred = await setup.resolveDeferred();
|
|
79
|
+
if (deferred?.deepLinkValue) {
|
|
80
|
+
setup.onDeepLink(deferred);
|
|
81
|
+
}
|
|
82
|
+
})();
|
|
83
|
+
|
|
84
|
+
return { remove: () => listener.remove(), ready };
|
|
85
|
+
}
|