@straitlink/capacitor 0.0.0-stage → 0.1.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/Package.swift +26 -0
- package/README.md +172 -2
- package/android/build.gradle +56 -0
- package/android/src/main/AndroidManifest.xml +3 -0
- package/android/src/main/java/dev/strait/capacitor/StraitPlugin.java +280 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +11 -0
- package/dist/strait.d.ts +94 -0
- package/dist/strait.js +69 -0
- package/ios/Sources/StraitPlugin/StraitPlugin.swift +129 -0
- package/package.json +69 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 The Strait authors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/Package.swift
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// swift-tools-version: 5.9
|
|
2
|
+
import PackageDescription
|
|
3
|
+
|
|
4
|
+
// @straitlink/capacitor, iOS side: a thin Capacitor plugin over StraitSDK (Swift Package Manager).
|
|
5
|
+
let package = Package(
|
|
6
|
+
name: "StraitlinkCapacitor",
|
|
7
|
+
platforms: [.iOS(.v15)],
|
|
8
|
+
products: [
|
|
9
|
+
.library(name: "StraitlinkCapacitor", targets: ["StraitPlugin"]),
|
|
10
|
+
],
|
|
11
|
+
dependencies: [
|
|
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.9.3"),
|
|
14
|
+
],
|
|
15
|
+
targets: [
|
|
16
|
+
.target(
|
|
17
|
+
name: "StraitPlugin",
|
|
18
|
+
dependencies: [
|
|
19
|
+
.product(name: "Capacitor", package: "capacitor-swift-pm"),
|
|
20
|
+
.product(name: "Cordova", package: "capacitor-swift-pm"),
|
|
21
|
+
.product(name: "StraitSDK", package: "strait-sdk-swift"),
|
|
22
|
+
],
|
|
23
|
+
path: "ios/Sources/StraitPlugin"
|
|
24
|
+
),
|
|
25
|
+
]
|
|
26
|
+
)
|
package/README.md
CHANGED
|
@@ -1,3 +1,173 @@
|
|
|
1
|
-
|
|
1
|
+
<!-- Header: the same in every Strait SDK README (design system v5). -->
|
|
2
|
+
<p align="center">
|
|
3
|
+
<a href="https://straitlink.in">
|
|
4
|
+
<picture>
|
|
5
|
+
<source media="(prefers-color-scheme: dark)" srcset=".github/assets/strait-lockup-dark.svg">
|
|
6
|
+
<img src=".github/assets/strait-lockup.svg" alt="Strait" width="160" height="53">
|
|
7
|
+
</picture>
|
|
8
|
+
</a>
|
|
9
|
+
</p>
|
|
2
10
|
|
|
3
|
-
|
|
11
|
+
<h1 align="center">Strait SDK for Capacitor and Ionic</h1>
|
|
12
|
+
|
|
13
|
+
<p align="center"><strong>Straight to the screen. On the record.</strong><br>
|
|
14
|
+
A tap opens the exact screen, and every link open and install is recorded in your Strait dashboard.</p>
|
|
15
|
+
|
|
16
|
+
<p align="center">
|
|
17
|
+
<a href="https://www.npmjs.com/package/@straitlink/capacitor"><img alt="npm" src="https://img.shields.io/npm/v/@straitlink/capacitor?style=flat-square&labelColor=0F0D0A&color=423B33"></a>
|
|
18
|
+
<a href="https://straitlink.in/platform-status/"><img alt="SDK: New" src="https://img.shields.io/badge/SDK-new-423B33?style=flat-square&labelColor=0F0D0A"></a>
|
|
19
|
+
<a href="https://github.com/strait-link/strait-sdk-capacitor/actions/workflows/ci.yml"><img alt="CI" src="https://img.shields.io/github/actions/workflow/status/strait-link/strait-sdk-capacitor/ci.yml?branch=main&label=CI&style=flat-square&labelColor=0F0D0A"></a>
|
|
20
|
+
<a href="LICENSE"><img alt="Licence: MIT" src="https://img.shields.io/badge/licence-MIT-423B33?style=flat-square&labelColor=0F0D0A"></a>
|
|
21
|
+
</p>
|
|
22
|
+
|
|
23
|
+
<p align="center">
|
|
24
|
+
<a href="https://straitlink.in/docs/sdks/capacitor/">Docs</a> ·
|
|
25
|
+
<a href="https://straitlink.in/platform-status/">Platform status</a> ·
|
|
26
|
+
<a href="CHANGELOG.md">Changelog</a> ·
|
|
27
|
+
<a href="#docs-and-support">Talk to the Strait team</a>
|
|
28
|
+
</p>
|
|
29
|
+
|
|
30
|
+
`@straitlink/capacitor` gives a Capacitor 8 app (Ionic, Vue, React, Angular or plain JS) direct
|
|
31
|
+
links (App Links, Universal Links, custom schemes), deferred links that survive the install, and
|
|
32
|
+
link-open analytics. It wraps the Strait [Android](https://github.com/strait-link/strait-sdk-android)
|
|
33
|
+
and [iOS](https://github.com/strait-link/strait-sdk-swift) SDKs, and its API matches
|
|
34
|
+
[`@straitlink/react-native`](https://github.com/strait-link/strait-sdk-react-native).
|
|
35
|
+
|
|
36
|
+
## Quick start (60 seconds)
|
|
37
|
+
|
|
38
|
+
You need: a Capacitor 8 app with `android/` and/or `ios/` added, and from
|
|
39
|
+
**Dashboard → Get started** your publishable key (`st_pub_live_…`) and link domain
|
|
40
|
+
(`https://<your-handle>.strait.link`).
|
|
41
|
+
|
|
42
|
+
**1. Install and sync**
|
|
43
|
+
|
|
44
|
+
```sh
|
|
45
|
+
npm install @straitlink/capacitor
|
|
46
|
+
npx cap sync
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
**2. Start it when your app starts** (for example in `main.ts`)
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
import { createStrait } from '@straitlink/capacitor';
|
|
53
|
+
|
|
54
|
+
const strait = createStrait({
|
|
55
|
+
publishableKey: 'st_pub_live_…', // safe in apps; never the secret key
|
|
56
|
+
endpoint: 'https://<your-handle>.strait.link',
|
|
57
|
+
});
|
|
58
|
+
strait.onLink((e) => {
|
|
59
|
+
console.log('Strait link:', e.kind, e.route, e.matched, e.path);
|
|
60
|
+
if (e.matched && e.path) router.push({ path: e.path, query: e.params }); // your router
|
|
61
|
+
});
|
|
62
|
+
strait.start();
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
**3. Let the app claim your link domain**
|
|
66
|
+
|
|
67
|
+
- Android, `android/app/src/main/AndroidManifest.xml`, inside the main `<activity>`:
|
|
68
|
+
|
|
69
|
+
```xml
|
|
70
|
+
<intent-filter android:autoVerify="true">
|
|
71
|
+
<action android:name="android.intent.action.VIEW" />
|
|
72
|
+
<category android:name="android.intent.category.DEFAULT" />
|
|
73
|
+
<category android:name="android.intent.category.BROWSABLE" />
|
|
74
|
+
<data android:scheme="https" android:host="<your-handle>.strait.link" />
|
|
75
|
+
</intent-filter>
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
- iOS, Xcode → App target → Signing & Capabilities → **+ Associated Domains** →
|
|
79
|
+
`applinks:<your-handle>.strait.link`.
|
|
80
|
+
- In **Dashboard → Settings → App**: your Android package name + signing SHA-256, and your
|
|
81
|
+
Apple Team ID + bundle ID. Strait then serves `assetlinks.json` and the
|
|
82
|
+
`apple-app-site-association` file for you.
|
|
83
|
+
|
|
84
|
+
**You should see:** run the app on a phone or emulator, then open one of your links:
|
|
85
|
+
|
|
86
|
+
```sh
|
|
87
|
+
adb shell am start -a android.intent.action.VIEW -d "https://<your-handle>.strait.link/<code>"
|
|
88
|
+
xcrun simctl openurl booted "https://<your-handle>.strait.link/<code>"
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
The app opens on the link's screen and the console prints
|
|
92
|
+
`Strait link: direct app_link true /p/42` (your path). The open shows up in the dashboard's
|
|
93
|
+
Analytics within a minute. On a fresh install the first launch prints a `deferred` event instead.
|
|
94
|
+
|
|
95
|
+
## Platform features
|
|
96
|
+
|
|
97
|
+
| Feature | Status | Notes |
|
|
98
|
+
|---|---|---|
|
|
99
|
+
| Android: direct links and deferred links | ● Live | Deferred = exact match through the Play Install Referrer (read for you). |
|
|
100
|
+
| iPhone links (Universal Links, custom schemes) | ● Live | Through Capacitor's AppDelegate / SceneDelegate proxy: no extra code. |
|
|
101
|
+
| iPhone: install matching (deferred links) | Opt-in | Your choice in Dashboard → Settings → iPhone installs, read at runtime. See the [iOS SDK](https://github.com/strait-link/strait-sdk-swift#iphone-you-choose-the-deferred-link-method). |
|
|
102
|
+
| iOS with CocoaPods | – Not yet | Swift Package Manager only (the Capacitor 8 default for new apps). |
|
|
103
|
+
| Web | – | Every call resolves a safe default; use [`@straitlink/web`](https://www.npmjs.com/package/@straitlink/web) for websites. |
|
|
104
|
+
| Published on npm | ● Live | `npm install @straitlink/capacitor` |
|
|
105
|
+
|
|
106
|
+
● Live · Opt-in: off until you switch it on · – Not yet. The same words as the
|
|
107
|
+
[platform status](https://straitlink.in/platform-status/) page.
|
|
108
|
+
|
|
109
|
+
## API
|
|
110
|
+
|
|
111
|
+
| Call | What it does |
|
|
112
|
+
|---|---|
|
|
113
|
+
| `createStrait({ publishableKey, endpoint, linkHosts? })` | One client per app. Throws only for a missing or secret key. `linkHosts` adds custom short-link hosts or old `<x>.page.link` hosts. |
|
|
114
|
+
| `start()` | Reads the link that launched the app, listens for new ones, runs the deferred check once per install, sends saved open reports. Call it once, after `onLink`. |
|
|
115
|
+
| `onLink(cb)` | Every link event, including ones from before you subscribed. Returns an unsubscribe. |
|
|
116
|
+
| `onLinkStart(cb)` | A link just arrived and is being resolved: show a loading state. |
|
|
117
|
+
| `checkDeferred()` | Asks for the deferred link again (settings or debug screens). Doesn't add an install. |
|
|
118
|
+
| `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. |
|
|
119
|
+
| `pendingOpenReports()` / `flushOpenReports()` | Open reports saved while offline (sent again on start and resume). |
|
|
120
|
+
| `deviceClass()` | `phone` or `tablet` (iPad / 600 dp and up), as sent with open reports. |
|
|
121
|
+
| `stop()` | Stops delivering events to this object. |
|
|
122
|
+
|
|
123
|
+
`LinkEvent`: `id, kind (direct|deferred), route (app_link|custom_scheme|install_referrer|fingerprint|clipboard),
|
|
124
|
+
appState (closed|background|foreground), matched, reason, rawUrl, url, path, params, linkId, referralCode, ms, at`.
|
|
125
|
+
After `createStrait`, nothing throws: a failure resolves `matched: false` with a `reason`, `false`, `0` or nothing.
|
|
126
|
+
|
|
127
|
+
## How it works
|
|
128
|
+
|
|
129
|
+
The plugin is a thin layer. All link logic (resolving short links, the once-per-install deferred
|
|
130
|
+
check, open reports and their retry queue, app-state labels) runs in the native SDKs, which share
|
|
131
|
+
golden test vectors with the Strait engine:
|
|
132
|
+
|
|
133
|
+
- **Android:** `strait-sdk-android` **v0.8.1** from JitPack. The plugin adds JitPack to your Gradle
|
|
134
|
+
repositories, limited to the `com.github.strait-link` group, so nothing else is ever fetched from
|
|
135
|
+
it. It reads the Play Install Referrer (`com.android.installreferrer:installreferrer:2.2`), the launch
|
|
136
|
+
intent and `onNewIntent`, and keeps state in SharedPreferences (`strait`).
|
|
137
|
+
- **iOS:** `StraitSDK` **0.9.3+** through Swift Package Manager. Links come from Capacitor's
|
|
138
|
+
`capacitorOpenURL` / `capacitorOpenUniversalLink` notifications; state lives in `UserDefaults`.
|
|
139
|
+
|
|
140
|
+
What gets recorded, the privacy details and the match methods are the same as the native SDKs:
|
|
141
|
+
see [Android](https://github.com/strait-link/strait-sdk-android#what-strait-records-automatically-no-extra-code)
|
|
142
|
+
and [iOS](https://github.com/strait-link/strait-sdk-swift#what-strait-records-automatically-no-extra-code).
|
|
143
|
+
|
|
144
|
+
### Testing a deferred link on an Android emulator
|
|
145
|
+
|
|
146
|
+
A sideloaded app never gets a Play Install Referrer. **Debug builds only** accept a stand-in on the
|
|
147
|
+
first launch after install (release builds ignore it):
|
|
148
|
+
|
|
149
|
+
```sh
|
|
150
|
+
adb shell am start -n <your.package>/.MainActivity \
|
|
151
|
+
--es strait.debugReferrer "'strait_link=lnk_…&strait_click=…'"
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Take both values from the Play Store URL your link redirects an Android phone to (its `referrer=`).
|
|
155
|
+
|
|
156
|
+
## Sample app
|
|
157
|
+
|
|
158
|
+
[`examples/capacitor-app`](examples/capacitor-app): a plain-JS Capacitor app with two screens
|
|
159
|
+
(Home and Product), a "Check deferred link" button and a "Log a purchase" button. CI builds it for
|
|
160
|
+
Android and the iOS Simulator on every pull request.
|
|
161
|
+
|
|
162
|
+
```sh
|
|
163
|
+
cd examples/capacitor-app && npm install
|
|
164
|
+
STRAIT_PUBLISHABLE_KEY=st_pub_live_… STRAIT_ENDPOINT=https://<your-handle>.strait.link npm run sync
|
|
165
|
+
npx cap run android # or: npx cap run ios
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## Docs and support
|
|
169
|
+
|
|
170
|
+
- **Docs:** [straitlink.in/docs/sdks/capacitor/](https://straitlink.in/docs/sdks/capacitor/) · [platform status](https://straitlink.in/platform-status/) · [troubleshooting](https://straitlink.in/docs/troubleshooting/)
|
|
171
|
+
- **Talk to the Strait team:** [support@straitlink.in](mailto:support@straitlink.in) (replies within 1 working day, IST) or call +91 81218 61890.
|
|
172
|
+
- **Bugs and feature requests:** [open an issue](https://github.com/strait-link/strait-sdk-capacitor/issues) on this repo.
|
|
173
|
+
- **Security:** never in a public issue. Write to security@straitlink.in (see [SECURITY.md](SECURITY.md)).
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// @straitlink/capacitor, Android side: a thin Capacitor plugin over strait-sdk-android (JitPack).
|
|
2
|
+
ext {
|
|
3
|
+
straitSdkVersion = 'v0.8.1'
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
buildscript {
|
|
7
|
+
repositories {
|
|
8
|
+
google()
|
|
9
|
+
mavenCentral()
|
|
10
|
+
}
|
|
11
|
+
dependencies {
|
|
12
|
+
classpath 'com.android.tools.build:gradle:8.13.0'
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
apply plugin: 'com.android.library'
|
|
17
|
+
|
|
18
|
+
// strait-sdk-android is built by JitPack from its version tags. Gradle resolves this plugin's
|
|
19
|
+
// dependencies with the app's repositories, so add JitPack to every project, limited to the
|
|
20
|
+
// Strait group (nothing else is ever fetched from JitPack).
|
|
21
|
+
rootProject.allprojects {
|
|
22
|
+
repositories {
|
|
23
|
+
maven {
|
|
24
|
+
url 'https://jitpack.io'
|
|
25
|
+
content { includeGroup 'com.github.strait-link' }
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
android {
|
|
31
|
+
namespace = 'dev.strait.capacitor'
|
|
32
|
+
compileSdk = project.hasProperty('compileSdkVersion') ? rootProject.ext.compileSdkVersion : 36
|
|
33
|
+
defaultConfig {
|
|
34
|
+
minSdkVersion project.hasProperty('minSdkVersion') ? rootProject.ext.minSdkVersion : 24
|
|
35
|
+
targetSdkVersion project.hasProperty('targetSdkVersion') ? rootProject.ext.targetSdkVersion : 36
|
|
36
|
+
}
|
|
37
|
+
compileOptions {
|
|
38
|
+
sourceCompatibility JavaVersion.VERSION_21
|
|
39
|
+
targetCompatibility JavaVersion.VERSION_21
|
|
40
|
+
}
|
|
41
|
+
lint {
|
|
42
|
+
abortOnError = false
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
repositories {
|
|
47
|
+
google()
|
|
48
|
+
mavenCentral()
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
dependencies {
|
|
52
|
+
implementation project(':capacitor-android')
|
|
53
|
+
implementation "androidx.appcompat:appcompat:${project.hasProperty('androidxAppCompatVersion') ? rootProject.ext.androidxAppCompatVersion : '1.7.1'}"
|
|
54
|
+
implementation "com.github.strait-link:strait-sdk-android:${straitSdkVersion}"
|
|
55
|
+
implementation 'com.android.installreferrer:installreferrer:2.2'
|
|
56
|
+
}
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
package dev.strait.capacitor;
|
|
2
|
+
|
|
3
|
+
import android.content.Context;
|
|
4
|
+
import android.content.Intent;
|
|
5
|
+
import android.content.SharedPreferences;
|
|
6
|
+
import android.content.pm.ApplicationInfo;
|
|
7
|
+
import android.util.DisplayMetrics;
|
|
8
|
+
|
|
9
|
+
import com.android.installreferrer.api.InstallReferrerClient;
|
|
10
|
+
import com.android.installreferrer.api.InstallReferrerStateListener;
|
|
11
|
+
import com.getcapacitor.JSArray;
|
|
12
|
+
import com.getcapacitor.JSObject;
|
|
13
|
+
import com.getcapacitor.Plugin;
|
|
14
|
+
import com.getcapacitor.PluginCall;
|
|
15
|
+
import com.getcapacitor.PluginMethod;
|
|
16
|
+
import com.getcapacitor.annotation.CapacitorPlugin;
|
|
17
|
+
|
|
18
|
+
import java.util.ArrayList;
|
|
19
|
+
import java.util.List;
|
|
20
|
+
import java.util.Locale;
|
|
21
|
+
import java.util.Map;
|
|
22
|
+
import java.util.TimeZone;
|
|
23
|
+
import java.util.concurrent.CountDownLatch;
|
|
24
|
+
import java.util.concurrent.ExecutorService;
|
|
25
|
+
import java.util.concurrent.Executors;
|
|
26
|
+
import java.util.concurrent.TimeUnit;
|
|
27
|
+
|
|
28
|
+
import dev.strait.sdk.AppLifecycleState;
|
|
29
|
+
import dev.strait.sdk.CoreKt;
|
|
30
|
+
import dev.strait.sdk.DeviceFields;
|
|
31
|
+
import dev.strait.sdk.KeyValueStore;
|
|
32
|
+
import dev.strait.sdk.LinkEvent;
|
|
33
|
+
import dev.strait.sdk.LinkStart;
|
|
34
|
+
import dev.strait.sdk.StraitClient;
|
|
35
|
+
import dev.strait.sdk.StraitConfig;
|
|
36
|
+
import dev.strait.sdk.UrlConnectionTransport;
|
|
37
|
+
import kotlin.Unit;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Capacitor bridge over strait-sdk-android's StraitClient. All link logic (resolve, deferred
|
|
41
|
+
* check once per install, open reports, retry queue) lives in the native SDK; this class only
|
|
42
|
+
* wires Android pieces (SharedPreferences, Play Install Referrer, display metrics, intents,
|
|
43
|
+
* lifecycle) and forwards events to JS as "link" / "linkStart".
|
|
44
|
+
*/
|
|
45
|
+
@CapacitorPlugin(name = "Strait")
|
|
46
|
+
public class StraitPlugin extends Plugin {
|
|
47
|
+
private final ExecutorService executor = Executors.newSingleThreadExecutor();
|
|
48
|
+
private StraitClient client;
|
|
49
|
+
/** A link that arrived (onNewIntent) before JS called start(). */
|
|
50
|
+
private String pendingUrl;
|
|
51
|
+
|
|
52
|
+
@PluginMethod
|
|
53
|
+
public synchronized void start(PluginCall call) {
|
|
54
|
+
if (client != null) { // a WebView reload calls start() again: keep the one client
|
|
55
|
+
call.resolve();
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
String key = call.getString("publishableKey", "");
|
|
59
|
+
String endpoint = call.getString("endpoint", "");
|
|
60
|
+
if (key == null || !key.startsWith("st_pub_") || endpoint == null || endpoint.isEmpty()) {
|
|
61
|
+
call.reject("Strait: start needs publishableKey (st_pub_...) and endpoint");
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
List<String> hosts = new ArrayList<>();
|
|
65
|
+
JSArray arr = call.getArray("linkHosts", new JSArray());
|
|
66
|
+
for (int i = 0; i < arr.length(); i++) {
|
|
67
|
+
String h = arr.optString(i, null);
|
|
68
|
+
if (h != null && !h.isEmpty()) hosts.add(h);
|
|
69
|
+
}
|
|
70
|
+
Context ctx = getContext().getApplicationContext();
|
|
71
|
+
SharedPreferences prefs = ctx.getSharedPreferences("strait", Context.MODE_PRIVATE);
|
|
72
|
+
KeyValueStore storage = new KeyValueStore() {
|
|
73
|
+
@Override public String get(String k) { return prefs.getString(k, null); }
|
|
74
|
+
@Override public void set(String k, String v) { prefs.edit().putString(k, v).apply(); }
|
|
75
|
+
};
|
|
76
|
+
Intent launch = getActivity() != null ? getActivity().getIntent() : null;
|
|
77
|
+
String debugReferrer = debugReferrer(ctx, launch);
|
|
78
|
+
client = new StraitClient(new StraitConfig(
|
|
79
|
+
key,
|
|
80
|
+
endpoint,
|
|
81
|
+
hosts,
|
|
82
|
+
storage,
|
|
83
|
+
() -> debugReferrer != null ? debugReferrer : readInstallReferrer(ctx),
|
|
84
|
+
"android",
|
|
85
|
+
() -> device(ctx),
|
|
86
|
+
new UrlConnectionTransport(10_000, 10_000),
|
|
87
|
+
executor,
|
|
88
|
+
System::currentTimeMillis,
|
|
89
|
+
deviceClass(ctx)
|
|
90
|
+
));
|
|
91
|
+
client.onLinkStart(s -> { notifyListeners("linkStart", toJs(s), true); return Unit.INSTANCE; });
|
|
92
|
+
client.onLink(e -> { notifyListeners("link", toJs(e), true); return Unit.INSTANCE; });
|
|
93
|
+
client.onAppState(AppLifecycleState.ACTIVE, System.currentTimeMillis());
|
|
94
|
+
|
|
95
|
+
String initial = pendingUrl != null ? pendingUrl : launchUrl(launch);
|
|
96
|
+
pendingUrl = null;
|
|
97
|
+
client.start(initial);
|
|
98
|
+
executor.execute(call::resolve); // after start's work on the same single thread
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
@PluginMethod
|
|
102
|
+
public void checkDeferred(PluginCall call) {
|
|
103
|
+
StraitClient c = ready(call);
|
|
104
|
+
if (c == null) return;
|
|
105
|
+
executor.execute(() -> call.resolve(toJs(c.checkDeferred())));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
@PluginMethod
|
|
109
|
+
public void trackEvent(PluginCall call) {
|
|
110
|
+
StraitClient c = ready(call);
|
|
111
|
+
if (c == null) return;
|
|
112
|
+
String name = call.getString("name", "");
|
|
113
|
+
if (name == null || name.isEmpty()) {
|
|
114
|
+
call.reject("Strait: trackEvent needs a name");
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
Double value = call.getDouble("value");
|
|
118
|
+
String currency = call.getString("currency");
|
|
119
|
+
String linkId = call.getString("linkId");
|
|
120
|
+
String clickId = call.getString("clickId");
|
|
121
|
+
executor.execute(() -> {
|
|
122
|
+
JSObject r = new JSObject();
|
|
123
|
+
r.put("accepted", c.trackEvent(name, value, currency, linkId, clickId));
|
|
124
|
+
call.resolve(r);
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
@PluginMethod
|
|
129
|
+
public void pendingOpenReports(PluginCall call) {
|
|
130
|
+
StraitClient c = ready(call);
|
|
131
|
+
if (c == null) return;
|
|
132
|
+
JSObject r = new JSObject();
|
|
133
|
+
r.put("count", c.pendingOpenReports());
|
|
134
|
+
call.resolve(r);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
@PluginMethod
|
|
138
|
+
public void flushOpenReports(PluginCall call) {
|
|
139
|
+
StraitClient c = ready(call);
|
|
140
|
+
if (c == null) return;
|
|
141
|
+
executor.execute(() -> { c.flushOpenReports(); call.resolve(); });
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
@PluginMethod
|
|
145
|
+
public void deviceClass(PluginCall call) {
|
|
146
|
+
JSObject r = new JSObject();
|
|
147
|
+
String dc = deviceClass(getContext());
|
|
148
|
+
if (dc != null) r.put("deviceClass", dc);
|
|
149
|
+
call.resolve(r);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
@Override
|
|
153
|
+
protected void handleOnNewIntent(Intent intent) {
|
|
154
|
+
super.handleOnNewIntent(intent);
|
|
155
|
+
String url = launchUrl(intent);
|
|
156
|
+
if (url == null) return;
|
|
157
|
+
synchronized (this) {
|
|
158
|
+
if (client == null) pendingUrl = url;
|
|
159
|
+
else client.handleUrl(url);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
@Override
|
|
164
|
+
protected void handleOnResume() {
|
|
165
|
+
super.handleOnResume();
|
|
166
|
+
if (client != null) client.onAppState(AppLifecycleState.ACTIVE, System.currentTimeMillis());
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
@Override
|
|
170
|
+
protected void handleOnPause() {
|
|
171
|
+
super.handleOnPause();
|
|
172
|
+
if (client != null) client.onAppState(AppLifecycleState.INACTIVE, System.currentTimeMillis());
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
@Override
|
|
176
|
+
protected void handleOnStop() {
|
|
177
|
+
super.handleOnStop();
|
|
178
|
+
if (client != null) client.onAppState(AppLifecycleState.BACKGROUND, System.currentTimeMillis());
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
private StraitClient ready(PluginCall call) {
|
|
182
|
+
if (client == null) call.reject("Strait: call start() first");
|
|
183
|
+
return client;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** The link in a VIEW intent, but not when Android replays an old intent from the recents screen. */
|
|
187
|
+
private static String launchUrl(Intent intent) {
|
|
188
|
+
if (intent == null || !Intent.ACTION_VIEW.equals(intent.getAction())) return null;
|
|
189
|
+
if ((intent.getFlags() & Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY) != 0) return null;
|
|
190
|
+
String s = intent.getDataString();
|
|
191
|
+
return s == null || s.isEmpty() ? null : s;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Debug builds only: a stand-in for the Play Install Referrer, so the deferred path can be tested
|
|
196
|
+
* on an emulator (a sideloaded app never gets a Play referrer). Release builds ignore it.
|
|
197
|
+
* adb shell am start -n <package>/.MainActivity --es strait.debugReferrer "strait_link=lnk_…&strait_click=…"
|
|
198
|
+
*/
|
|
199
|
+
private static String debugReferrer(Context ctx, Intent intent) {
|
|
200
|
+
if (intent == null || (ctx.getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE) == 0) return null;
|
|
201
|
+
String r = intent.getStringExtra("strait.debugReferrer");
|
|
202
|
+
return r == null || r.isEmpty() ? null : r;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
private static DeviceFields device(Context ctx) {
|
|
206
|
+
DisplayMetrics m = ctx.getResources().getDisplayMetrics();
|
|
207
|
+
return new DeviceFields(
|
|
208
|
+
CoreKt.portraitScreenWidth(m.widthPixels / (double) m.density, m.heightPixels / (double) m.density),
|
|
209
|
+
m.density,
|
|
210
|
+
Locale.getDefault().toLanguageTag(),
|
|
211
|
+
TimeZone.getDefault().getID()
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** Contract B14: the sw600dp rule (600 dp or more = tablet). */
|
|
216
|
+
private static String deviceClass(Context ctx) {
|
|
217
|
+
return CoreKt.deviceClassForSize(ctx.getResources().getConfiguration().smallestScreenWidthDp);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** Blocking read of the Play Install Referrer (runs on the SDK's executor), or null. */
|
|
221
|
+
private static String readInstallReferrer(Context ctx) {
|
|
222
|
+
CountDownLatch latch = new CountDownLatch(1);
|
|
223
|
+
String[] result = {null};
|
|
224
|
+
InstallReferrerClient rc = InstallReferrerClient.newBuilder(ctx).build();
|
|
225
|
+
try {
|
|
226
|
+
rc.startConnection(new InstallReferrerStateListener() {
|
|
227
|
+
@Override public void onInstallReferrerSetupFinished(int code) {
|
|
228
|
+
if (code == InstallReferrerClient.InstallReferrerResponse.OK) {
|
|
229
|
+
try { result[0] = rc.getInstallReferrer().getInstallReferrer(); } catch (Exception ignored) { }
|
|
230
|
+
}
|
|
231
|
+
latch.countDown();
|
|
232
|
+
}
|
|
233
|
+
@Override public void onInstallReferrerServiceDisconnected() { latch.countDown(); }
|
|
234
|
+
});
|
|
235
|
+
latch.await(5, TimeUnit.SECONDS);
|
|
236
|
+
} catch (Exception ignored) {
|
|
237
|
+
} finally {
|
|
238
|
+
try { rc.endConnection(); } catch (Exception ignored) { }
|
|
239
|
+
}
|
|
240
|
+
return result[0];
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
static JSObject toJs(LinkEvent e) {
|
|
244
|
+
JSObject o = new JSObject();
|
|
245
|
+
o.put("id", e.getId());
|
|
246
|
+
o.put("kind", e.getKind());
|
|
247
|
+
o.put("route", e.getRoute());
|
|
248
|
+
o.put("appState", e.getAppState());
|
|
249
|
+
o.put("matched", e.getMatched());
|
|
250
|
+
putIf(o, "reason", e.getReason());
|
|
251
|
+
putIf(o, "rawUrl", e.getRawUrl());
|
|
252
|
+
putIf(o, "url", e.getUrl());
|
|
253
|
+
putIf(o, "path", e.getPath());
|
|
254
|
+
putIf(o, "linkId", e.getLinkId());
|
|
255
|
+
putIf(o, "referralCode", e.getReferralCode());
|
|
256
|
+
Map<String, String> params = e.getParams();
|
|
257
|
+
if (params != null) {
|
|
258
|
+
JSObject p = new JSObject();
|
|
259
|
+
for (Map.Entry<String, String> kv : params.entrySet()) p.put(kv.getKey(), kv.getValue());
|
|
260
|
+
o.put("params", p);
|
|
261
|
+
}
|
|
262
|
+
o.put("ms", e.getMs());
|
|
263
|
+
o.put("at", e.getAt());
|
|
264
|
+
return o;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
static JSObject toJs(LinkStart s) {
|
|
268
|
+
JSObject o = new JSObject();
|
|
269
|
+
o.put("id", s.getId());
|
|
270
|
+
o.put("kind", s.getKind());
|
|
271
|
+
o.put("appState", s.getAppState());
|
|
272
|
+
putIf(o, "rawUrl", s.getRawUrl());
|
|
273
|
+
o.put("at", s.getAt());
|
|
274
|
+
return o;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
private static void putIf(JSObject o, String k, String v) {
|
|
278
|
+
if (v != null) o.put(k, v);
|
|
279
|
+
}
|
|
280
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type CreateStraitConfig, type Strait, type StraitNativePlugin } from './strait.js';
|
|
2
|
+
/** The native plugin (registered as "Strait"). Most apps use `createStrait` instead. */
|
|
3
|
+
export declare const StraitNative: StraitNativePlugin;
|
|
4
|
+
/**
|
|
5
|
+
* Create one client when the app starts, subscribe with `onLink`, then call `start()`.
|
|
6
|
+
* Android and iOS only; on the web every call resolves a safe default (use @straitlink/web there).
|
|
7
|
+
*/
|
|
8
|
+
export declare function createStrait(config: CreateStraitConfig, native?: StraitNativePlugin): Strait;
|
|
9
|
+
export type { CreateStraitConfig, DeviceClass, LinkEvent, LinkStart, Strait, StraitNativePlugin, TrackEventExtra, } from './strait.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { registerPlugin } from '@capacitor/core';
|
|
2
|
+
import { createStrait as createWith } from './strait.js';
|
|
3
|
+
/** The native plugin (registered as "Strait"). Most apps use `createStrait` instead. */
|
|
4
|
+
export const StraitNative = registerPlugin('Strait');
|
|
5
|
+
/**
|
|
6
|
+
* Create one client when the app starts, subscribe with `onLink`, then call `start()`.
|
|
7
|
+
* Android and iOS only; on the web every call resolves a safe default (use @straitlink/web there).
|
|
8
|
+
*/
|
|
9
|
+
export function createStrait(config, native = StraitNative) {
|
|
10
|
+
return createWith(config, native);
|
|
11
|
+
}
|
package/dist/strait.d.ts
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type { PluginListenerHandle } from '@capacitor/core';
|
|
2
|
+
/** One link the app received (B9): the same shape as @straitlink/react-native. */
|
|
3
|
+
export interface LinkEvent {
|
|
4
|
+
/** Unique per open; also the id Strait records this open under. */
|
|
5
|
+
id: string;
|
|
6
|
+
/** direct = the app was opened by a link; deferred = link tapped before install. */
|
|
7
|
+
kind: 'direct' | 'deferred';
|
|
8
|
+
/** app_link · custom_scheme · install_referrer · fingerprint · clipboard. */
|
|
9
|
+
route: 'app_link' | 'custom_scheme' | 'install_referrer' | 'fingerprint' | 'clipboard';
|
|
10
|
+
/** What the app was doing when the link arrived. */
|
|
11
|
+
appState: 'closed' | 'background' | 'foreground';
|
|
12
|
+
matched: boolean;
|
|
13
|
+
/** Why it didn't match: not_found, expired, password_protected, network, no_match, unavailable, … */
|
|
14
|
+
reason?: string;
|
|
15
|
+
/** The URL the OS gave the app (direct links). */
|
|
16
|
+
rawUrl?: string;
|
|
17
|
+
/** The destination to navigate to. */
|
|
18
|
+
url?: string;
|
|
19
|
+
path?: string;
|
|
20
|
+
params?: Record<string, string>;
|
|
21
|
+
linkId?: string;
|
|
22
|
+
/** Deferred links only: the referral code the tap carried (preview, B21). */
|
|
23
|
+
referralCode?: string;
|
|
24
|
+
/** Time spent resolving, ms. */
|
|
25
|
+
ms: number;
|
|
26
|
+
at: number;
|
|
27
|
+
}
|
|
28
|
+
/** A link just arrived and is being resolved: show a loading state until the LinkEvent with the same id. */
|
|
29
|
+
export interface LinkStart {
|
|
30
|
+
id: string;
|
|
31
|
+
kind: 'direct' | 'deferred';
|
|
32
|
+
appState: 'closed' | 'background' | 'foreground';
|
|
33
|
+
rawUrl?: string;
|
|
34
|
+
at: number;
|
|
35
|
+
}
|
|
36
|
+
export type DeviceClass = 'phone' | 'tablet';
|
|
37
|
+
export interface CreateStraitConfig {
|
|
38
|
+
/** Your workspace's publishable key (`st_pub_live_…`), Dashboard → Get started. Never the secret key. */
|
|
39
|
+
publishableKey: string;
|
|
40
|
+
/** Your workspace's link domain, e.g. https://<your-handle>.strait.link */
|
|
41
|
+
endpoint: string;
|
|
42
|
+
/** Extra hosts that serve your short links (custom domains, old `<x>.page.link`). */
|
|
43
|
+
linkHosts?: string[];
|
|
44
|
+
}
|
|
45
|
+
export interface TrackEventExtra {
|
|
46
|
+
value?: number;
|
|
47
|
+
currency?: string;
|
|
48
|
+
linkId?: string;
|
|
49
|
+
clickId?: string;
|
|
50
|
+
}
|
|
51
|
+
/** The native side (Android: StraitPlugin.java over strait-sdk-android; iOS: StraitPlugin.swift over StraitSDK). */
|
|
52
|
+
export interface StraitNativePlugin {
|
|
53
|
+
start(options: {
|
|
54
|
+
publishableKey: string;
|
|
55
|
+
endpoint: string;
|
|
56
|
+
linkHosts: string[];
|
|
57
|
+
}): Promise<void>;
|
|
58
|
+
checkDeferred(): Promise<LinkEvent>;
|
|
59
|
+
trackEvent(options: {
|
|
60
|
+
name: string;
|
|
61
|
+
} & TrackEventExtra): Promise<{
|
|
62
|
+
accepted: boolean;
|
|
63
|
+
}>;
|
|
64
|
+
pendingOpenReports(): Promise<{
|
|
65
|
+
count: number;
|
|
66
|
+
}>;
|
|
67
|
+
flushOpenReports(): Promise<void>;
|
|
68
|
+
deviceClass(): Promise<{
|
|
69
|
+
deviceClass?: DeviceClass;
|
|
70
|
+
}>;
|
|
71
|
+
addListener(event: 'link', cb: (e: LinkEvent) => void): Promise<PluginListenerHandle>;
|
|
72
|
+
addListener(event: 'linkStart', cb: (s: LinkStart) => void): Promise<PluginListenerHandle>;
|
|
73
|
+
}
|
|
74
|
+
export interface Strait {
|
|
75
|
+
/** Reads the launch link, listens for new ones, runs the deferred check once per install. */
|
|
76
|
+
start(): Promise<void>;
|
|
77
|
+
/** Every link event, including ones that happened before you subscribed. Returns an unsubscribe. */
|
|
78
|
+
onLink(cb: (event: LinkEvent) => void): () => void;
|
|
79
|
+
/** A link just arrived and is being resolved (for a loading state). Returns an unsubscribe. */
|
|
80
|
+
onLinkStart(cb: (start: LinkStart) => void): () => void;
|
|
81
|
+
/** Re-run the deferred check now (debugging); doesn't touch the once-per-install flag. */
|
|
82
|
+
checkDeferred(): Promise<LinkEvent>;
|
|
83
|
+
/** Conversion / revenue event, on the tap of the last attributed open (≤7 days, B15). True when accepted. */
|
|
84
|
+
trackEvent(name: string, extra?: TrackEventExtra): Promise<boolean>;
|
|
85
|
+
/** Open reports saved while offline, waiting to be sent (debugging). */
|
|
86
|
+
pendingOpenReports(): Promise<number>;
|
|
87
|
+
/** Send saved open reports now (also happens on start and on resume). */
|
|
88
|
+
flushOpenReports(): Promise<void>;
|
|
89
|
+
/** What the app runs on, as sent with open reports (B14): iPad / sw600dp = tablet. */
|
|
90
|
+
deviceClass(): Promise<DeviceClass | undefined>;
|
|
91
|
+
/** Stop delivering events to this object. */
|
|
92
|
+
stop(): void;
|
|
93
|
+
}
|
|
94
|
+
export declare function createStrait(config: CreateStraitConfig, native: StraitNativePlugin): Strait;
|
package/dist/strait.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
export function createStrait(config, native) {
|
|
2
|
+
const key = config.publishableKey ?? '';
|
|
3
|
+
if (!key.startsWith('st_pub_')) {
|
|
4
|
+
throw new TypeError('Strait: use your publishable key (st_pub_…, Dashboard → Get started). A secret key must never ship inside an app.');
|
|
5
|
+
}
|
|
6
|
+
const events = [];
|
|
7
|
+
const linkCbs = new Set();
|
|
8
|
+
const startCbs = new Set();
|
|
9
|
+
const each = (cbs, x) => {
|
|
10
|
+
for (const cb of [...cbs]) {
|
|
11
|
+
try {
|
|
12
|
+
cb(x);
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
// an app's handler must not stop the others
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
// Subscribe before start(), so the link that launched the app is never missed.
|
|
20
|
+
const handles = [
|
|
21
|
+
native.addListener('link', (e) => {
|
|
22
|
+
events.push(e);
|
|
23
|
+
each(linkCbs, e);
|
|
24
|
+
}),
|
|
25
|
+
native.addListener('linkStart', (s) => each(startCbs, s)),
|
|
26
|
+
];
|
|
27
|
+
let started;
|
|
28
|
+
const safe = async (fn, fallback) => {
|
|
29
|
+
try {
|
|
30
|
+
return await fn();
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return fallback;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
return {
|
|
37
|
+
start() {
|
|
38
|
+
started ??= safe(async () => {
|
|
39
|
+
await Promise.all(handles);
|
|
40
|
+
await native.start({ publishableKey: key, endpoint: config.endpoint, linkHosts: config.linkHosts ?? [] });
|
|
41
|
+
}, undefined);
|
|
42
|
+
return started;
|
|
43
|
+
},
|
|
44
|
+
onLink(cb) {
|
|
45
|
+
linkCbs.add(cb);
|
|
46
|
+
for (const e of [...events])
|
|
47
|
+
each(new Set([cb]), e);
|
|
48
|
+
return () => linkCbs.delete(cb);
|
|
49
|
+
},
|
|
50
|
+
onLinkStart(cb) {
|
|
51
|
+
startCbs.add(cb);
|
|
52
|
+
return () => startCbs.delete(cb);
|
|
53
|
+
},
|
|
54
|
+
checkDeferred: () => safe(() => native.checkDeferred(), {
|
|
55
|
+
id: '', kind: 'deferred', route: 'fingerprint', appState: 'foreground',
|
|
56
|
+
matched: false, reason: 'unavailable', ms: 0, at: Date.now(),
|
|
57
|
+
}),
|
|
58
|
+
trackEvent: (name, extra = {}) => safe(async () => (await native.trackEvent({ name, ...extra })).accepted === true, false),
|
|
59
|
+
pendingOpenReports: () => safe(async () => (await native.pendingOpenReports()).count ?? 0, 0),
|
|
60
|
+
flushOpenReports: () => safe(() => native.flushOpenReports(), undefined),
|
|
61
|
+
deviceClass: () => safe(async () => (await native.deviceClass()).deviceClass, undefined),
|
|
62
|
+
stop() {
|
|
63
|
+
linkCbs.clear();
|
|
64
|
+
startCbs.clear();
|
|
65
|
+
for (const h of handles)
|
|
66
|
+
void h.then((x) => x.remove()).catch(() => { });
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import Capacitor
|
|
3
|
+
import StraitSDK
|
|
4
|
+
|
|
5
|
+
/// Capacitor bridge over StraitSDK's `StraitLinks`. All link logic (resolve, deferred check once
|
|
6
|
+
/// per install, open reports, retry queue, app-state labels) lives in the native SDK; this class
|
|
7
|
+
/// only feeds it the links Capacitor's AppDelegate proxy receives and forwards events to JS as
|
|
8
|
+
/// "link" / "linkStart".
|
|
9
|
+
@objc(StraitPlugin)
|
|
10
|
+
public class StraitPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
11
|
+
public let identifier = "StraitPlugin"
|
|
12
|
+
public let jsName = "Strait"
|
|
13
|
+
public let pluginMethods: [CAPPluginMethod] = [
|
|
14
|
+
CAPPluginMethod(name: "start", returnType: CAPPluginReturnPromise),
|
|
15
|
+
CAPPluginMethod(name: "checkDeferred", returnType: CAPPluginReturnPromise),
|
|
16
|
+
CAPPluginMethod(name: "trackEvent", returnType: CAPPluginReturnPromise),
|
|
17
|
+
CAPPluginMethod(name: "pendingOpenReports", returnType: CAPPluginReturnPromise),
|
|
18
|
+
CAPPluginMethod(name: "flushOpenReports", returnType: CAPPluginReturnPromise),
|
|
19
|
+
CAPPluginMethod(name: "deviceClass", returnType: CAPPluginReturnPromise),
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
private var links: StraitLinks?
|
|
23
|
+
private var subscriptions: [StraitSubscription] = []
|
|
24
|
+
/// A link that arrived before JS called start().
|
|
25
|
+
private var pendingURL: URL?
|
|
26
|
+
|
|
27
|
+
override public func load() {
|
|
28
|
+
for name in [Notification.Name.capacitorOpenURL, .capacitorOpenUniversalLink] {
|
|
29
|
+
NotificationCenter.default.addObserver(self, selector: #selector(onOpen(_:)), name: name, object: nil)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
@objc private func onOpen(_ note: Notification) {
|
|
34
|
+
guard let url = (note.object as? [String: Any])?["url"] as? URL else { return }
|
|
35
|
+
DispatchQueue.main.async { [self] in
|
|
36
|
+
if let links = links { links.handle(url: url) } else { pendingURL = url }
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
@objc func start(_ call: CAPPluginCall) {
|
|
41
|
+
DispatchQueue.main.async { [self] in
|
|
42
|
+
if links != nil { call.resolve(); return } // a WebView reload calls start() again
|
|
43
|
+
let key = call.getString("publishableKey") ?? ""
|
|
44
|
+
let endpoint = call.getString("endpoint") ?? ""
|
|
45
|
+
guard key.hasPrefix("st_pub_"), !endpoint.isEmpty else {
|
|
46
|
+
call.reject("Strait: start needs publishableKey (st_pub_...) and endpoint")
|
|
47
|
+
return
|
|
48
|
+
}
|
|
49
|
+
let hosts = (call.getArray("linkHosts") as? [String]) ?? []
|
|
50
|
+
let l = StraitLinks(StraitLinksConfig(publishableKey: key, endpoint: endpoint, linkHosts: hosts))
|
|
51
|
+
links = l
|
|
52
|
+
subscriptions = [
|
|
53
|
+
l.onLinkStart { [weak self] s in self?.notifyListeners("linkStart", data: Self.js(s), retainUntilConsumed: true) },
|
|
54
|
+
l.onLink { [weak self] e in self?.notifyListeners("link", data: Self.js(e), retainUntilConsumed: true) },
|
|
55
|
+
]
|
|
56
|
+
// The launch link: delivered before start() (cold start), or remembered by Capacitor.
|
|
57
|
+
let initial = pendingURL ?? ApplicationDelegateProxy.shared.lastURL
|
|
58
|
+
pendingURL = nil
|
|
59
|
+
l.start(initialURL: initial) { call.resolve() }
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
@objc func checkDeferred(_ call: CAPPluginCall) {
|
|
64
|
+
guard let l = ready(call) else { return }
|
|
65
|
+
l.checkDeferred { e in call.resolve(Self.js(e)) }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
@objc func trackEvent(_ call: CAPPluginCall) {
|
|
69
|
+
guard let l = ready(call) else { return }
|
|
70
|
+
guard let name = call.getString("name"), !name.isEmpty else {
|
|
71
|
+
call.reject("Strait: trackEvent needs a name")
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
l.trackEvent(
|
|
75
|
+
name,
|
|
76
|
+
value: call.getDouble("value"),
|
|
77
|
+
currency: call.getString("currency"),
|
|
78
|
+
linkId: call.getString("linkId"),
|
|
79
|
+
clickId: call.getString("clickId")
|
|
80
|
+
) { ok in call.resolve(["accepted": ok]) }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
@objc func pendingOpenReports(_ call: CAPPluginCall) {
|
|
84
|
+
guard let l = ready(call) else { return }
|
|
85
|
+
l.pendingOpenReports { n in call.resolve(["count": n]) }
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
@objc func flushOpenReports(_ call: CAPPluginCall) {
|
|
89
|
+
guard let l = ready(call) else { return }
|
|
90
|
+
l.flushOpenReports { call.resolve() }
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
@objc func deviceClass(_ call: CAPPluginCall) {
|
|
94
|
+
DispatchQueue.main.async {
|
|
95
|
+
var r: [String: Any] = [:]
|
|
96
|
+
if let dc = currentDeviceClass() { r["deviceClass"] = dc }
|
|
97
|
+
call.resolve(r)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private func ready(_ call: CAPPluginCall) -> StraitLinks? {
|
|
102
|
+
guard let l = links else {
|
|
103
|
+
call.reject("Strait: call start() first")
|
|
104
|
+
return nil
|
|
105
|
+
}
|
|
106
|
+
return l
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
static func js(_ e: LinkEvent) -> [String: Any] {
|
|
110
|
+
var o: [String: Any] = [
|
|
111
|
+
"id": e.id, "kind": e.kind.rawValue, "route": e.route.rawValue, "appState": e.appState.rawValue,
|
|
112
|
+
"matched": e.matched, "ms": e.ms, "at": e.at,
|
|
113
|
+
]
|
|
114
|
+
if let v = e.reason { o["reason"] = v }
|
|
115
|
+
if let v = e.rawUrl { o["rawUrl"] = v }
|
|
116
|
+
if let v = e.url { o["url"] = v }
|
|
117
|
+
if let v = e.path { o["path"] = v }
|
|
118
|
+
if let v = e.params { o["params"] = v }
|
|
119
|
+
if let v = e.linkId { o["linkId"] = v }
|
|
120
|
+
if let v = e.referralCode { o["referralCode"] = v }
|
|
121
|
+
return o
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
static func js(_ s: LinkStart) -> [String: Any] {
|
|
125
|
+
var o: [String: Any] = ["id": s.id, "kind": s.kind.rawValue, "appState": s.appState.rawValue, "at": s.at]
|
|
126
|
+
if let v = s.rawUrl { o["rawUrl"] = v }
|
|
127
|
+
return o
|
|
128
|
+
}
|
|
129
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,71 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@straitlink/capacitor",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
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
|
+
"keywords": [
|
|
6
|
+
"capacitor",
|
|
7
|
+
"capacitor-plugin",
|
|
8
|
+
"ionic",
|
|
9
|
+
"deep-linking",
|
|
10
|
+
"deferred-deep-linking",
|
|
11
|
+
"universal-links",
|
|
12
|
+
"app-links",
|
|
13
|
+
"dynamic-links"
|
|
14
|
+
],
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"author": "The Strait authors",
|
|
17
|
+
"homepage": "https://straitlink.in/docs/sdks/capacitor/",
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/strait-link/strait-sdk-capacitor.git"
|
|
21
|
+
},
|
|
22
|
+
"bugs": {
|
|
23
|
+
"url": "https://github.com/strait-link/strait-sdk-capacitor/issues"
|
|
24
|
+
},
|
|
25
|
+
"type": "module",
|
|
26
|
+
"main": "dist/index.js",
|
|
27
|
+
"module": "dist/index.js",
|
|
28
|
+
"types": "dist/index.d.ts",
|
|
29
|
+
"exports": {
|
|
30
|
+
".": {
|
|
31
|
+
"types": "./dist/index.d.ts",
|
|
32
|
+
"import": "./dist/index.js",
|
|
33
|
+
"default": "./dist/index.js"
|
|
34
|
+
},
|
|
35
|
+
"./package.json": "./package.json"
|
|
36
|
+
},
|
|
37
|
+
"files": [
|
|
38
|
+
"dist",
|
|
39
|
+
"android/src/main/",
|
|
40
|
+
"android/build.gradle",
|
|
41
|
+
"ios/Sources",
|
|
42
|
+
"Package.swift"
|
|
43
|
+
],
|
|
44
|
+
"capacitor": {
|
|
45
|
+
"ios": {
|
|
46
|
+
"src": "ios"
|
|
47
|
+
},
|
|
48
|
+
"android": {
|
|
49
|
+
"src": "android"
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
"sideEffects": false,
|
|
53
|
+
"peerDependencies": {
|
|
54
|
+
"@capacitor/core": ">=8.0.0"
|
|
55
|
+
},
|
|
56
|
+
"publishConfig": {
|
|
57
|
+
"access": "public",
|
|
58
|
+
"provenance": true
|
|
59
|
+
},
|
|
60
|
+
"scripts": {
|
|
61
|
+
"build": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.json",
|
|
62
|
+
"prepublishOnly": "npm run build",
|
|
63
|
+
"test": "vitest run",
|
|
64
|
+
"typecheck": "tsc --noEmit"
|
|
65
|
+
},
|
|
66
|
+
"devDependencies": {
|
|
67
|
+
"@capacitor/core": "8.5.3",
|
|
68
|
+
"typescript": "^5.7.0",
|
|
69
|
+
"vitest": "^4.1.12"
|
|
70
|
+
}
|
|
71
|
+
}
|