@straitlink/capacitor 0.2.0 → 0.4.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/Package.swift +1 -1
- package/README.md +38 -4
- package/android/build.gradle +2 -2
- package/android/src/main/java/dev/strait/capacitor/StraitPlugin.java +95 -3
- package/dist/index.d.ts +1 -1
- package/dist/strait.d.ts +53 -0
- package/dist/strait.js +8 -1
- package/ios/Sources/StraitPlugin/StraitPlugin.swift +60 -1
- package/package.json +1 -1
package/Package.swift
CHANGED
|
@@ -10,7 +10,7 @@ let package = Package(
|
|
|
10
10
|
],
|
|
11
11
|
dependencies: [
|
|
12
12
|
.package(url: "https://github.com/ionic-team/capacitor-swift-pm.git", from: "8.0.0"),
|
|
13
|
-
.package(url: "https://github.com/strait-link/strait-sdk-swift.git", from: "0.
|
|
13
|
+
.package(url: "https://github.com/strait-link/strait-sdk-swift.git", from: "0.11.0"), // setEnabled + F7 + B6a/B13a/C1 (StraitSDK 0.11.0)
|
|
14
14
|
],
|
|
15
15
|
targets: [
|
|
16
16
|
.target(
|
package/README.md
CHANGED
|
@@ -33,12 +33,23 @@ link-open analytics. It wraps the Strait [Android](https://github.com/strait-lin
|
|
|
33
33
|
and [iOS](https://github.com/strait-link/strait-sdk-swift) SDKs, and its API matches
|
|
34
34
|
[`@straitlink/react-native`](https://github.com/strait-link/strait-sdk-react-native).
|
|
35
35
|
|
|
36
|
+
## Guided setup
|
|
37
|
+
|
|
38
|
+
There's no `strait init` for Capacitor yet: the three steps below are the whole setup. Or let your AI coding tool do it: connect the [Strait MCP server](https://straitlink.in/docs/mcp/) and run its `get_started` prompt with platform `capacitor`; it pastes the exact setup from the docs. Then check that your link domain lists your app:
|
|
39
|
+
|
|
40
|
+
```sh
|
|
41
|
+
npx @straitlink/cli doctor --host <your-handle>.strait.link --android <your.package> --ios <your.bundle.id>
|
|
42
|
+
```
|
|
43
|
+
|
|
36
44
|
## Quick start (60 seconds)
|
|
37
45
|
|
|
38
46
|
You need: a Capacitor 8 app with `android/` and/or `ios/` added, and from
|
|
39
47
|
**Dashboard → Get started** your publishable key (`st_pub_live_…`) and link domain
|
|
40
48
|
(`https://<your-handle>.strait.link`).
|
|
41
49
|
|
|
50
|
+
Android builds of a Capacitor 8 app need JDK 21 (Capacitor's own requirement; with JDK 17 the
|
|
51
|
+
Gradle build fails). Android Studio's bundled JDK is 21; on the command line check `java -version`.
|
|
52
|
+
|
|
42
53
|
**1. Install and sync**
|
|
43
54
|
|
|
44
55
|
```sh
|
|
@@ -105,6 +116,11 @@ for (const f of report.findings) console.log(f.severity, f.id, f.title, f.fix);
|
|
|
105
116
|
`error` comes with the fix, for example `cert.missing`: "Add AB:CD:… in Dashboard → Apps & stores.
|
|
106
117
|
Debug, release and Play App Signing keys each have their own fingerprint."
|
|
107
118
|
|
|
119
|
+
## User consent / opt-out
|
|
120
|
+
|
|
121
|
+
If your app asks for consent first (for example under India's DPDP Act or a GDPR consent flow in the EU), call `await strait.setEnabled(false)` until the person agrees: Strait then sends no open reports or events, does no install matching, and deletes reports waiting on the device. The choice is kept across launches on the phone; `setEnabled(true)` turns it back on (default: on) and `isEnabled()` reads it. It works before `start()`, so a first-launch consent screen can call it first; `setEnabled(true)` then runs the deferred check.
|
|
122
|
+
Links that carry their own destination still open your screens. A Strait short link is looked up through the engine's `/v1/lookup`, which records nothing (no tap, no open, no tap id), so it still opens the right screen; only if that lookup fails (offline) does it come back `matched: false, reason: 'disabled'`. Whether you need consent, and for what, is a question for your own legal advice.
|
|
123
|
+
|
|
108
124
|
## Platform features
|
|
109
125
|
|
|
110
126
|
| Feature | Status | Notes |
|
|
@@ -128,9 +144,11 @@ Debug, release and Play App Signing keys each have their own fingerprint."
|
|
|
128
144
|
| `onLink(cb)` | Every link event, including ones from before you subscribed. Returns an unsubscribe. |
|
|
129
145
|
| `onLinkStart(cb)` | A link just arrived and is being resolved: show a loading state. |
|
|
130
146
|
| `checkDeferred()` | Asks for the deferred link again (settings or debug screens). Doesn't add an install. |
|
|
131
|
-
| `trackEvent(name, { value, currency, linkId, clickId })` | Conversion or revenue event, placed on the tap of the last attributed open (7 days). Resolves `true` when accepted. |
|
|
132
|
-
| `pendingOpenReports()` / `flushOpenReports()` | Open reports saved while offline (sent again on start and resume). |
|
|
147
|
+
| `trackEvent(name, { value, currency, linkId, clickId, eventId })` | Conversion or revenue event, placed on the tap of the last attributed open (7 days). `eventId` = your order id, stored once (left out, the SDK makes one). Resolves `true` when accepted; an event that can't be sent (offline, server busy) is kept and sent later with the open reports. Revenue you must trust: send it from your server with the secret key (the publishable key is public). |
|
|
148
|
+
| `pendingOpenReports()` / `flushOpenReports()` | Open reports saved while offline (sent again on start and resume). After a 429/5xx the queue waits (15 s up to 1 h, at least the server's `Retry-After`) before sending again. |
|
|
133
149
|
| `deviceClass()` | `phone` or `tablet` (iPad / 600 dp and up), as sent with open reports. |
|
|
150
|
+
| `openStoreSheet(url, options?)` | The app store inside your app for one of your links (another app to install), deep link kept through the install. Options: `androidPackage`, `listing`, `inline`, `appStoreId`, `providerToken`, `customProductPageId`, `style`. Resolves `{ opened, method, clickId, linkId, referrer, reason }`. |
|
|
151
|
+
| `setEnabled(enabled)` / `isEnabled()` | Opt-out switch for consent flows (see above). Kept on the phone; works before `start()`. |
|
|
134
152
|
| `doctor()` | Setup check for debug builds (key, endpoint, this app listed in your link domain's files, Android signing certificate). Resolves `{ ok, findings }`. |
|
|
135
153
|
| `stop()` | Stops delivering events to this object. |
|
|
136
154
|
|
|
@@ -144,13 +162,18 @@ The plugin is a thin layer. All link logic (resolving short links, the once-per-
|
|
|
144
162
|
check, open reports and their retry queue, app-state labels) runs in the native SDKs, which share
|
|
145
163
|
golden test vectors with the Strait engine:
|
|
146
164
|
|
|
147
|
-
- **Android:** `strait-sdk-android` **v0.
|
|
165
|
+
- **Android:** `strait-sdk-android` **v0.10.0** from JitPack (the version is pinned in `android/build.gradle`). The plugin adds JitPack to your Gradle
|
|
148
166
|
repositories, limited to the `com.github.strait-link` group, so nothing else is ever fetched from
|
|
149
167
|
it. It reads the Play Install Referrer (`com.android.installreferrer:installreferrer:2.2`), the launch
|
|
150
168
|
intent and `onNewIntent`, and keeps state in SharedPreferences (`strait`).
|
|
151
|
-
- **iOS:** `StraitSDK` **0.
|
|
169
|
+
- **iOS:** `StraitSDK` **0.11.0+** through Swift Package Manager (pinned in `Package.swift`). Links come from Capacitor's
|
|
152
170
|
`capacitorOpenURL` / `capacitorOpenUniversalLink` notifications; state lives in `UserDefaults`.
|
|
153
171
|
|
|
172
|
+
**Reinstalls (contract B6a):** Android Auto Backup and restored iPhones bring the "deferred check
|
|
173
|
+
done" flag back after a reinstall. The plugin passes the app's install time to the native SDKs
|
|
174
|
+
(Android `firstInstallTime`; iOS a marker file kept out of backups), so a reinstall from a link still
|
|
175
|
+
gets its deferred link and is counted. An app update is never a new install. No app code needed.
|
|
176
|
+
|
|
154
177
|
What gets recorded, the privacy details and the match methods are the same as the native SDKs:
|
|
155
178
|
see [Android](https://github.com/strait-link/strait-sdk-android#what-strait-records-automatically-no-extra-code)
|
|
156
179
|
and [iOS](https://github.com/strait-link/strait-sdk-swift#what-strait-records-automatically-no-extra-code).
|
|
@@ -167,6 +190,17 @@ adb shell am start -n <your.package>/.MainActivity \
|
|
|
167
190
|
|
|
168
191
|
Take both values from the Play Store URL your link redirects an Android phone to (its `referrer=`).
|
|
169
192
|
|
|
193
|
+
## Size and speed
|
|
194
|
+
|
|
195
|
+
Measured 25 Oct 2026 on 0.3.0.
|
|
196
|
+
|
|
197
|
+
| What | Size or time | How it was measured |
|
|
198
|
+
|---|---|---|
|
|
199
|
+
| npm package | 15.6 KB download, 50 KB unpacked (JS, plus the Android and iOS plugin sources) | `npm pack --dry-run` |
|
|
200
|
+
| In your web bundle | 1.5 KB minified, 0.8 KB gzip (`@capacitor/core` is yours already) | esbuild 0.25 `--bundle --minify` |
|
|
201
|
+
| Native | the Android SDK (about 35 KB of dex after R8) and the Swift SDK (about 280 KB of arm64 code before App Store compression); see their READMEs | measured in those repos |
|
|
202
|
+
| Start-up (JS side) | 3.0 ms to load the module with `@capacitor/core` + 0.07 ms for `createStrait(...)` | fresh Node 26 process, median of 25. The native side's start-up is in the Android and Swift READMEs |
|
|
203
|
+
|
|
170
204
|
## Sample app
|
|
171
205
|
|
|
172
206
|
[`examples/capacitor-app`](examples/capacitor-app): a plain-JS Capacitor app with two screens
|
package/android/build.gradle
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// @straitlink/capacitor, Android side: a thin Capacitor plugin over strait-sdk-android (JitPack).
|
|
2
2
|
ext {
|
|
3
|
-
straitSdkVersion = 'v0.
|
|
3
|
+
straitSdkVersion = 'v0.10.0' // setEnabled + B6a/B13a/C1 (strait-sdk-android v0.10.0)
|
|
4
4
|
}
|
|
5
5
|
|
|
6
6
|
buildscript {
|
|
@@ -15,7 +15,7 @@ buildscript {
|
|
|
15
15
|
|
|
16
16
|
apply plugin: 'com.android.library'
|
|
17
17
|
|
|
18
|
-
// strait-sdk-android is built by JitPack from its version tags. Gradle resolves this plugin's
|
|
18
|
+
// strait-sdk-android is built by JitPack from its version tags (or, between releases, a main commit). Gradle resolves this plugin's
|
|
19
19
|
// dependencies with the app's repositories, so add JitPack to every project, limited to the
|
|
20
20
|
// Strait group (nothing else is ever fetched from JitPack).
|
|
21
21
|
rootProject.allprojects {
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
package dev.strait.capacitor;
|
|
2
2
|
|
|
3
3
|
import android.content.Context;
|
|
4
|
+
import android.content.ActivityNotFoundException;
|
|
4
5
|
import android.content.Intent;
|
|
6
|
+
import android.net.Uri;
|
|
5
7
|
import android.content.SharedPreferences;
|
|
6
8
|
import android.content.pm.ApplicationInfo;
|
|
7
9
|
import android.content.pm.PackageInfo;
|
|
@@ -39,6 +41,9 @@ import dev.strait.sdk.DoctorReport;
|
|
|
39
41
|
import dev.strait.sdk.KeyValueStore;
|
|
40
42
|
import dev.strait.sdk.LinkEvent;
|
|
41
43
|
import dev.strait.sdk.LinkStart;
|
|
44
|
+
import dev.strait.sdk.StoreLauncher;
|
|
45
|
+
import dev.strait.sdk.StoreSheetOptions;
|
|
46
|
+
import dev.strait.sdk.StoreSheetResult;
|
|
42
47
|
import dev.strait.sdk.StraitClient;
|
|
43
48
|
import dev.strait.sdk.StraitConfig;
|
|
44
49
|
import dev.strait.sdk.UrlConnectionTransport;
|
|
@@ -52,10 +57,14 @@ import kotlin.Unit;
|
|
|
52
57
|
*/
|
|
53
58
|
@CapacitorPlugin(name = "Strait")
|
|
54
59
|
public class StraitPlugin extends Plugin {
|
|
60
|
+
/** strait-sdk-android's opt-out key in the "strait" preferences: "0" = off, absent = on. */
|
|
61
|
+
private static final String ENABLED_KEY = "strait.enabled";
|
|
55
62
|
private final ExecutorService executor = Executors.newSingleThreadExecutor();
|
|
56
63
|
private StraitClient client;
|
|
57
64
|
/** A link that arrived (onNewIntent) before JS called start(). */
|
|
58
65
|
private String pendingUrl;
|
|
66
|
+
/** setEnabled() before start(): applied to the client before it starts (which also clears saved reports). */
|
|
67
|
+
private Boolean pendingEnabled;
|
|
59
68
|
|
|
60
69
|
@PluginMethod
|
|
61
70
|
public synchronized void start(PluginCall call) {
|
|
@@ -76,7 +85,7 @@ public class StraitPlugin extends Plugin {
|
|
|
76
85
|
if (h != null && !h.isEmpty()) hosts.add(h);
|
|
77
86
|
}
|
|
78
87
|
Context ctx = getContext().getApplicationContext();
|
|
79
|
-
SharedPreferences prefs = ctx
|
|
88
|
+
SharedPreferences prefs = prefs(ctx);
|
|
80
89
|
KeyValueStore storage = new KeyValueStore() {
|
|
81
90
|
@Override public String get(String k) { return prefs.getString(k, null); }
|
|
82
91
|
@Override public void set(String k, String v) { prefs.edit().putString(k, v).apply(); }
|
|
@@ -94,8 +103,11 @@ public class StraitPlugin extends Plugin {
|
|
|
94
103
|
new UrlConnectionTransport(10_000, 10_000),
|
|
95
104
|
executor,
|
|
96
105
|
System::currentTimeMillis,
|
|
97
|
-
deviceClass(ctx)
|
|
106
|
+
deviceClass(ctx),
|
|
107
|
+
() -> installTime(ctx) // B6a: Auto Backup restores the flag; a new install time re-runs the check
|
|
98
108
|
));
|
|
109
|
+
if (pendingEnabled != null) client.setEnabled(pendingEnabled); // before anything is sent
|
|
110
|
+
pendingEnabled = null;
|
|
99
111
|
client.onLinkStart(s -> { notifyListeners("linkStart", toJs(s), true); return Unit.INSTANCE; });
|
|
100
112
|
client.onLink(e -> { notifyListeners("link", toJs(e), true); return Unit.INSTANCE; });
|
|
101
113
|
client.onAppState(AppLifecycleState.ACTIVE, System.currentTimeMillis());
|
|
@@ -106,6 +118,44 @@ public class StraitPlugin extends Plugin {
|
|
|
106
118
|
executor.execute(call::resolve); // after start's work on the same single thread
|
|
107
119
|
}
|
|
108
120
|
|
|
121
|
+
/** Opt-out switch (consent): the native SDK's setEnabled. Before start() it is saved and applied at start. */
|
|
122
|
+
@PluginMethod
|
|
123
|
+
public synchronized void setEnabled(PluginCall call) {
|
|
124
|
+
Boolean on = call.getBoolean("enabled");
|
|
125
|
+
if (on == null) {
|
|
126
|
+
call.reject("Strait: setEnabled needs enabled (true or false)");
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (client != null) {
|
|
130
|
+
client.setEnabled(on);
|
|
131
|
+
executor.execute(call::resolve); // after the SDK's clearing / re-run on the same single thread
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
pendingEnabled = on;
|
|
135
|
+
prefs(getContext()).edit().putString(ENABLED_KEY, on ? "1" : "0").apply();
|
|
136
|
+
call.resolve();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
@PluginMethod
|
|
140
|
+
public synchronized void isEnabled(PluginCall call) {
|
|
141
|
+
JSObject r = new JSObject();
|
|
142
|
+
r.put("enabled", client != null ? client.isEnabled() : !"0".equals(prefs(getContext()).getString(ENABLED_KEY, null)));
|
|
143
|
+
call.resolve(r);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
private static SharedPreferences prefs(Context ctx) {
|
|
147
|
+
return ctx.getApplicationContext().getSharedPreferences("strait", Context.MODE_PRIVATE);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** This install's time (PackageInfo.firstInstallTime, epoch ms), or null; contract B6a. */
|
|
151
|
+
private static Long installTime(Context ctx) {
|
|
152
|
+
try {
|
|
153
|
+
return ctx.getPackageManager().getPackageInfo(ctx.getPackageName(), 0).firstInstallTime;
|
|
154
|
+
} catch (Exception e) {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
109
159
|
@PluginMethod
|
|
110
160
|
public void checkDeferred(PluginCall call) {
|
|
111
161
|
StraitClient c = ready(call);
|
|
@@ -126,9 +176,10 @@ public class StraitPlugin extends Plugin {
|
|
|
126
176
|
String currency = call.getString("currency");
|
|
127
177
|
String linkId = call.getString("linkId");
|
|
128
178
|
String clickId = call.getString("clickId");
|
|
179
|
+
String eventId = call.getString("eventId");
|
|
129
180
|
executor.execute(() -> {
|
|
130
181
|
JSObject r = new JSObject();
|
|
131
|
-
r.put("accepted", c.trackEvent(name, value, currency, linkId, clickId));
|
|
182
|
+
r.put("accepted", c.trackEvent(name, value, currency, linkId, clickId, eventId));
|
|
132
183
|
call.resolve(r);
|
|
133
184
|
});
|
|
134
185
|
}
|
|
@@ -184,6 +235,47 @@ public class StraitPlugin extends Plugin {
|
|
|
184
235
|
});
|
|
185
236
|
}
|
|
186
237
|
|
|
238
|
+
/** Store sheet: Google Play inside the app for one of the workspace's links, deep link kept. */
|
|
239
|
+
@PluginMethod
|
|
240
|
+
public void openStoreSheet(PluginCall call) {
|
|
241
|
+
StraitClient c = ready(call);
|
|
242
|
+
if (c == null) return;
|
|
243
|
+
String url = call.getString("url", "");
|
|
244
|
+
if (url == null || url.isEmpty()) {
|
|
245
|
+
call.reject("Strait: openStoreSheet needs a url");
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
Context ctx = getContext();
|
|
249
|
+
StoreSheetOptions opts = new StoreSheetOptions(
|
|
250
|
+
call.getString("androidPackage"), ctx.getPackageName(), call.getString("listing"), call.getBoolean("inline", true));
|
|
251
|
+
StoreLauncher launcher = s -> {
|
|
252
|
+
Intent intent = new Intent(s.getAction(), Uri.parse(s.getData()));
|
|
253
|
+
if (s.getPackageName() != null) intent.setPackage(s.getPackageName());
|
|
254
|
+
for (Map.Entry<String, Object> kv : s.getExtras().entrySet()) {
|
|
255
|
+
if (kv.getValue() instanceof Boolean) intent.putExtra(kv.getKey(), (Boolean) kv.getValue());
|
|
256
|
+
else if (kv.getValue() instanceof String) intent.putExtra(kv.getKey(), (String) kv.getValue());
|
|
257
|
+
}
|
|
258
|
+
if (getActivity() == null) intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
|
259
|
+
try {
|
|
260
|
+
(getActivity() != null ? getActivity() : ctx).startActivity(intent);
|
|
261
|
+
return true;
|
|
262
|
+
} catch (ActivityNotFoundException e) {
|
|
263
|
+
return false;
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
executor.execute(() -> {
|
|
267
|
+
StoreSheetResult r = c.openStoreSheet(url, launcher, opts);
|
|
268
|
+
JSObject o = new JSObject();
|
|
269
|
+
o.put("opened", r.getOpened());
|
|
270
|
+
o.put("method", r.getMethod());
|
|
271
|
+
putIf(o, "clickId", r.getClickId());
|
|
272
|
+
putIf(o, "linkId", r.getLinkId());
|
|
273
|
+
putIf(o, "referrer", r.getReferrer());
|
|
274
|
+
putIf(o, "reason", r.getReason());
|
|
275
|
+
call.resolve(o);
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
187
279
|
/** SHA-256 of this build's signing certificate(s), "AB:CD:…" (what assetlinks.json lists). */
|
|
188
280
|
@SuppressWarnings("deprecation")
|
|
189
281
|
private static List<String> signingCerts(Context ctx) {
|
package/dist/index.d.ts
CHANGED
|
@@ -6,4 +6,4 @@ export declare const StraitNative: StraitNativePlugin;
|
|
|
6
6
|
* Android and iOS only; on the web every call resolves a safe default (use @straitlink/web there).
|
|
7
7
|
*/
|
|
8
8
|
export declare function createStrait(config: CreateStraitConfig, native?: StraitNativePlugin): Strait;
|
|
9
|
-
export type { CreateStraitConfig, DeviceClass, DoctorFinding, DoctorReport, LinkEvent, LinkStart, Strait, StraitNativePlugin, TrackEventExtra, } from './strait.js';
|
|
9
|
+
export type { CreateStraitConfig, DeviceClass, DoctorFinding, DoctorReport, LinkEvent, LinkStart, Strait, StoreSheetOptions, StoreSheetResult, StraitNativePlugin, TrackEventExtra, } from './strait.js';
|
package/dist/strait.d.ts
CHANGED
|
@@ -42,6 +42,32 @@ export interface DoctorFinding {
|
|
|
42
42
|
/** What to do; empty for `ok`. */
|
|
43
43
|
fix: string;
|
|
44
44
|
}
|
|
45
|
+
/** Store sheet options (same names as @straitlink/react-native). */
|
|
46
|
+
export interface StoreSheetOptions {
|
|
47
|
+
/** Android: the app to install. Default: the workspace's Android package from Strait. */
|
|
48
|
+
androidPackage?: string;
|
|
49
|
+
/** Android: a Play custom store listing name. */
|
|
50
|
+
listing?: string;
|
|
51
|
+
/** Android: false skips Play's inline half-sheet. Default true. */
|
|
52
|
+
inline?: boolean;
|
|
53
|
+
/** iPhone: the app to install. Default: the workspace's App Store ID from Strait. */
|
|
54
|
+
appStoreId?: string;
|
|
55
|
+
providerToken?: string;
|
|
56
|
+
customProductPageId?: string;
|
|
57
|
+
/** iPhone: the full product page (default) or a small card at the bottom. */
|
|
58
|
+
style?: 'product_page' | 'overlay';
|
|
59
|
+
}
|
|
60
|
+
export interface StoreSheetResult {
|
|
61
|
+
opened: boolean;
|
|
62
|
+
/** inline_install | market | web | product_page | overlay | none */
|
|
63
|
+
method: string;
|
|
64
|
+
clickId?: string;
|
|
65
|
+
linkId?: string;
|
|
66
|
+
/** Android: the Play Install Referrer sent. */
|
|
67
|
+
referrer?: string;
|
|
68
|
+
/** not_found, expired, offline, no_package, no_app_store_id, not_shown, unavailable… */
|
|
69
|
+
reason?: string;
|
|
70
|
+
}
|
|
45
71
|
export interface DoctorReport {
|
|
46
72
|
/** No `error` finding. */
|
|
47
73
|
ok: boolean;
|
|
@@ -60,6 +86,8 @@ export interface TrackEventExtra {
|
|
|
60
86
|
currency?: string;
|
|
61
87
|
linkId?: string;
|
|
62
88
|
clickId?: string;
|
|
89
|
+
/** Your id for this conversion, e.g. the order id (1-64 of A-Z a-z 0-9 _ . : -). Stored once (B13a). */
|
|
90
|
+
eventId?: string;
|
|
63
91
|
}
|
|
64
92
|
/** The native side (Android: StraitPlugin.java over strait-sdk-android; iOS: StraitPlugin.swift over StraitSDK). */
|
|
65
93
|
export interface StraitNativePlugin {
|
|
@@ -82,6 +110,15 @@ export interface StraitNativePlugin {
|
|
|
82
110
|
deviceClass?: DeviceClass;
|
|
83
111
|
}>;
|
|
84
112
|
doctor(): Promise<DoctorReport>;
|
|
113
|
+
openStoreSheet(options: {
|
|
114
|
+
url: string;
|
|
115
|
+
} & StoreSheetOptions): Promise<StoreSheetResult>;
|
|
116
|
+
setEnabled(options: {
|
|
117
|
+
enabled: boolean;
|
|
118
|
+
}): Promise<void>;
|
|
119
|
+
isEnabled(): Promise<{
|
|
120
|
+
enabled: boolean;
|
|
121
|
+
}>;
|
|
85
122
|
addListener(event: 'link', cb: (e: LinkEvent) => void): Promise<PluginListenerHandle>;
|
|
86
123
|
addListener(event: 'linkStart', cb: (s: LinkStart) => void): Promise<PluginListenerHandle>;
|
|
87
124
|
}
|
|
@@ -108,6 +145,22 @@ export interface Strait {
|
|
|
108
145
|
* signing certificate). Call after start(). Never throws.
|
|
109
146
|
*/
|
|
110
147
|
doctor(): Promise<DoctorReport>;
|
|
148
|
+
/**
|
|
149
|
+
* Store sheet: open the app store inside your app for one of your short links (an app you
|
|
150
|
+
* want people to install), keeping the deep link through the install. Never throws.
|
|
151
|
+
*/
|
|
152
|
+
openStoreSheet(url: string, options?: StoreSheetOptions): Promise<StoreSheetResult>;
|
|
153
|
+
/**
|
|
154
|
+
* Opt-out switch for consent flows (default on, kept on the phone across launches by the native SDK).
|
|
155
|
+
* Off: no open reports, no events, no deferred or clipboard matching, nothing remembered; reports saved
|
|
156
|
+
* earlier and the remembered tap are deleted. A link that carries its own destination still reaches
|
|
157
|
+
* onLink; a short link is looked up through /v1/lookup, which records nothing (no tap, no open, no tap
|
|
158
|
+
* id), so it still routes (`reason: 'disabled'` only if that lookup fails). Back on: sending resumes, and
|
|
159
|
+
* a first-launch check skipped while off runs now. Works before start(): call it first on a consent screen.
|
|
160
|
+
*/
|
|
161
|
+
setEnabled(enabled: boolean): Promise<void>;
|
|
162
|
+
/** The opt-out switch's current value (see setEnabled). True when it can't be read (the default). */
|
|
163
|
+
isEnabled(): Promise<boolean>;
|
|
111
164
|
/** Stop delivering events to this object. */
|
|
112
165
|
stop(): void;
|
|
113
166
|
}
|
package/dist/strait.js
CHANGED
|
@@ -55,11 +55,18 @@ export function createStrait(config, native) {
|
|
|
55
55
|
id: '', kind: 'deferred', route: 'fingerprint', appState: 'foreground',
|
|
56
56
|
matched: false, reason: 'unavailable', ms: 0, at: Date.now(),
|
|
57
57
|
}),
|
|
58
|
-
trackEvent: (name, extra = {}) => safe(async () =>
|
|
58
|
+
trackEvent: (name, extra = {}) => safe(async () => {
|
|
59
|
+
// Native getString() reads a number as missing: send a plain-JS number order id as its string (B13a).
|
|
60
|
+
const eventId = extra.eventId != null && extra.eventId !== '' ? { eventId: String(extra.eventId) } : {};
|
|
61
|
+
return (await native.trackEvent({ name, ...extra, ...eventId })).accepted === true;
|
|
62
|
+
}, false),
|
|
59
63
|
pendingOpenReports: () => safe(async () => (await native.pendingOpenReports()).count ?? 0, 0),
|
|
60
64
|
flushOpenReports: () => safe(() => native.flushOpenReports(), undefined),
|
|
61
65
|
deviceClass: () => safe(async () => (await native.deviceClass()).deviceClass, undefined),
|
|
62
66
|
doctor: () => safe(() => native.doctor(), { ok: false, findings: [] }),
|
|
67
|
+
openStoreSheet: (url, options = {}) => safe(() => native.openStoreSheet({ url, ...options }), { opened: false, method: 'none', reason: 'unavailable' }),
|
|
68
|
+
setEnabled: (enabled) => safe(() => native.setEnabled({ enabled: enabled === true }), undefined),
|
|
69
|
+
isEnabled: () => safe(async () => (await native.isEnabled()).enabled !== false, true),
|
|
63
70
|
stop() {
|
|
64
71
|
linkCbs.clear();
|
|
65
72
|
startCbs.clear();
|
|
@@ -18,12 +18,17 @@ public class StraitPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
18
18
|
CAPPluginMethod(name: "flushOpenReports", returnType: CAPPluginReturnPromise),
|
|
19
19
|
CAPPluginMethod(name: "deviceClass", returnType: CAPPluginReturnPromise),
|
|
20
20
|
CAPPluginMethod(name: "doctor", returnType: CAPPluginReturnPromise),
|
|
21
|
+
CAPPluginMethod(name: "openStoreSheet", returnType: CAPPluginReturnPromise),
|
|
22
|
+
CAPPluginMethod(name: "setEnabled", returnType: CAPPluginReturnPromise),
|
|
23
|
+
CAPPluginMethod(name: "isEnabled", returnType: CAPPluginReturnPromise),
|
|
21
24
|
]
|
|
22
25
|
|
|
23
26
|
private var links: StraitLinks?
|
|
24
27
|
private var subscriptions: [StraitSubscription] = []
|
|
25
28
|
/// A link that arrived before JS called start().
|
|
26
29
|
private var pendingURL: URL?
|
|
30
|
+
/// setEnabled() before start(): applied to StraitLinks before it starts (which also clears saved reports).
|
|
31
|
+
private var pendingEnabled: Bool?
|
|
27
32
|
|
|
28
33
|
override public func load() {
|
|
29
34
|
for name in [Notification.Name.capacitorOpenURL, .capacitorOpenUniversalLink] {
|
|
@@ -50,6 +55,8 @@ public class StraitPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
50
55
|
let hosts = (call.getArray("linkHosts") as? [String]) ?? []
|
|
51
56
|
let l = StraitLinks(StraitLinksConfig(publishableKey: key, endpoint: endpoint, linkHosts: hosts))
|
|
52
57
|
links = l
|
|
58
|
+
if let on = pendingEnabled { l.setEnabled(on) } // before anything is sent
|
|
59
|
+
pendingEnabled = nil
|
|
53
60
|
subscriptions = [
|
|
54
61
|
l.onLinkStart { [weak self] s in self?.notifyListeners("linkStart", data: Self.js(s), retainUntilConsumed: true) },
|
|
55
62
|
l.onLink { [weak self] e in self?.notifyListeners("link", data: Self.js(e), retainUntilConsumed: true) },
|
|
@@ -61,6 +68,27 @@ public class StraitPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
61
68
|
}
|
|
62
69
|
}
|
|
63
70
|
|
|
71
|
+
/// Opt-out switch (consent): StraitSDK's setEnabled. Before start() it is saved (StraitLinks' default
|
|
72
|
+
/// storage, UserDefaults) and applied at start.
|
|
73
|
+
@objc func setEnabled(_ call: CAPPluginCall) {
|
|
74
|
+
guard let on = call.getBool("enabled") else {
|
|
75
|
+
call.reject("Strait: setEnabled needs enabled (true or false)")
|
|
76
|
+
return
|
|
77
|
+
}
|
|
78
|
+
DispatchQueue.main.async { [self] in
|
|
79
|
+
if let l = links { l.setEnabled(on) { call.resolve() }; return }
|
|
80
|
+
pendingEnabled = on
|
|
81
|
+
UserDefaultsStorage().setItem(StraitLinks.enabledKey, on ? "1" : "0")
|
|
82
|
+
call.resolve()
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
@objc func isEnabled(_ call: CAPPluginCall) {
|
|
87
|
+
DispatchQueue.main.async { [self] in
|
|
88
|
+
call.resolve(["enabled": links?.isEnabled ?? (UserDefaultsStorage().getItem(StraitLinks.enabledKey) != "0")])
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
64
92
|
@objc func checkDeferred(_ call: CAPPluginCall) {
|
|
65
93
|
guard let l = ready(call) else { return }
|
|
66
94
|
l.checkDeferred { e in call.resolve(Self.js(e)) }
|
|
@@ -77,7 +105,8 @@ public class StraitPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
77
105
|
value: call.getDouble("value"),
|
|
78
106
|
currency: call.getString("currency"),
|
|
79
107
|
linkId: call.getString("linkId"),
|
|
80
|
-
clickId: call.getString("clickId")
|
|
108
|
+
clickId: call.getString("clickId"),
|
|
109
|
+
eventId: call.getString("eventId")
|
|
81
110
|
) { ok in call.resolve(["accepted": ok]) }
|
|
82
111
|
}
|
|
83
112
|
|
|
@@ -111,6 +140,36 @@ public class StraitPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
111
140
|
}
|
|
112
141
|
}
|
|
113
142
|
|
|
143
|
+
/// Store sheet: the App Store inside the app for one of the workspace's links, deep link kept.
|
|
144
|
+
@objc func openStoreSheet(_ call: CAPPluginCall) {
|
|
145
|
+
guard let l = ready(call) else { return }
|
|
146
|
+
guard let url = call.getString("url"), !url.isEmpty else {
|
|
147
|
+
call.reject("Strait: openStoreSheet needs a url")
|
|
148
|
+
return
|
|
149
|
+
}
|
|
150
|
+
DispatchQueue.main.async { [weak self] in
|
|
151
|
+
guard let vc = self?.bridge?.viewController else {
|
|
152
|
+
call.resolve(["opened": false, "method": "none", "reason": "no_view"])
|
|
153
|
+
return
|
|
154
|
+
}
|
|
155
|
+
let options = StoreSheetOptions(
|
|
156
|
+
appStoreId: call.getString("appStoreId"),
|
|
157
|
+
providerToken: call.getString("providerToken"),
|
|
158
|
+
customProductPageId: call.getString("customProductPageId"),
|
|
159
|
+
style: StoreSheetStyle(rawValue: call.getString("style") ?? "") ?? .productPage
|
|
160
|
+
)
|
|
161
|
+
let presenter = SystemStoreSheetPresenter(from: vc)
|
|
162
|
+
l.openStoreSheet(url: url, options: options, presenter: presenter) { r in
|
|
163
|
+
_ = presenter // keep the presenter (the store sheet's delegate) alive until done
|
|
164
|
+
var o: [String: Any] = ["opened": r.opened, "method": r.method]
|
|
165
|
+
if let v = r.clickId { o["clickId"] = v }
|
|
166
|
+
if let v = r.linkId { o["linkId"] = v }
|
|
167
|
+
if let v = r.reason { o["reason"] = v }
|
|
168
|
+
call.resolve(o)
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
114
173
|
private func ready(_ call: CAPPluginCall) -> StraitLinks? {
|
|
115
174
|
guard let l = links else {
|
|
116
175
|
call.reject("Strait: call start() first")
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@straitlink/capacitor",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Deep linking for Capacitor and Ionic apps: direct links (App Links, Universal Links, custom schemes), deferred links that survive the install and link-open analytics. Wraps the Strait Android and iOS SDKs.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"capacitor",
|