@octane-xplat/platform 0.1.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/README.md +37 -0
- package/package.json +21 -8
- package/src/a11y.native.ts +9 -5
- package/src/a11y.web.ts +13 -9
- package/src/app-info.native.ts +35 -0
- package/src/app-info.web.ts +10 -0
- package/src/biometrics.native.ts +10 -10
- package/src/biometrics.web.ts +9 -17
- package/src/clipboard.native.ts +28 -15
- package/src/clipboard.web.ts +29 -17
- package/src/connectivity.native.ts +86 -0
- package/src/connectivity.web.ts +62 -0
- package/src/deep-links.native.ts +47 -20
- package/src/deep-links.web.ts +16 -11
- package/src/device.native.ts +3 -3
- package/src/device.web.ts +5 -4
- package/src/files.native.ts +122 -16
- package/src/files.web.ts +20 -19
- package/src/geolocation.native.ts +44 -0
- package/src/geolocation.web.ts +50 -0
- package/src/haptics.native.ts +14 -10
- package/src/haptics.web.ts +5 -4
- package/src/index.ts +49 -22
- package/src/lifecycle.native.ts +1 -1
- package/src/lifecycle.native.tsrx +2 -0
- package/src/lifecycle.web.ts +1 -1
- package/src/lifecycle.web.tsrx +1 -0
- package/src/locale.native.ts +3 -3
- package/src/locale.web.ts +5 -9
- package/src/media.native.ts +74 -14
- package/src/media.web.ts +71 -9
- package/src/notifications.native.ts +7 -7
- package/src/notifications.web.ts +18 -7
- package/src/open-url.native.ts +45 -0
- package/src/open-url.web.ts +15 -0
- package/src/permissions.native.ts +10 -10
- package/src/permissions.web.ts +13 -31
- package/src/safe-area.native.ts +1 -1
- package/src/safe-area.native.tsrx +76 -17
- package/src/safe-area.web.ts +1 -1
- package/src/safe-area.web.tsrx +3 -0
- package/src/screen.native.ts +1 -1
- package/src/screen.native.tsrx +1 -0
- package/src/screen.web.ts +1 -1
- package/src/screen.web.tsrx +1 -0
- package/src/secure-storage.native.ts +5 -5
- package/src/secure-storage.web.ts +2 -2
- package/src/share.native.ts +6 -6
- package/src/share.web.ts +17 -13
- package/src/storage.native.ts +8 -4
- package/src/storage.web.ts +7 -3
- package/src/system-bars.native.ts +10 -8
- package/src/system-bars.web.ts +9 -6
- package/src/types.ts +101 -32
package/README.md
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# @octane-xplat/platform
|
|
2
|
+
|
|
3
|
+
> Headless platform capabilities for Octane xplat — one shared API surface,
|
|
4
|
+
> resolved per target (`*.web` under web conditions, `*.native`/`.ios`/
|
|
5
|
+
> `.android` under native).
|
|
6
|
+
>
|
|
7
|
+
> Status: `0.x` — the API surface is still moving.
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
pnpm add @octane-xplat/platform octane
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
`octane` is a required peer; the `@nativescript/*` peers are optional and
|
|
14
|
+
only needed for native targets.
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
import { storage, permissions } from '@octane-xplat/platform'
|
|
18
|
+
|
|
19
|
+
storage.setString('has-seen-welcome', 'true')
|
|
20
|
+
const seen = storage.getString('has-seen-welcome')
|
|
21
|
+
|
|
22
|
+
const result = await permissions.ensure('camera')
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Services cover storage, permissions, share, haptics, media picking, deep
|
|
26
|
+
links, and more. The full capability map lives in the docs:
|
|
27
|
+
|
|
28
|
+
- [Using device features](https://octane-xplat.goddardai.org/platform-services) — guide
|
|
29
|
+
- [Platform-service notes](https://octane-xplat.goddardai.org/notes/platform-notes) — per-capability impl map
|
|
30
|
+
|
|
31
|
+
Rules for consumers:
|
|
32
|
+
|
|
33
|
+
- Import from the package (`'@octane-xplat/platform'` or a submodule like
|
|
34
|
+
`'@octane-xplat/platform/storage'`) — never a leaf impl file.
|
|
35
|
+
- Optional capabilities return `{ supported: boolean }` rather than throwing.
|
|
36
|
+
- This package is headless only. UI-shaped plugins (drawer, menu) are leaf
|
|
37
|
+
primitives in [`@octane-xplat/ui`](https://octane-xplat.goddardai.org/primitives).
|
package/package.json
CHANGED
|
@@ -1,32 +1,45 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@octane-xplat/platform",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "Headless platform capabilities for Octane xplat — web + NativeScript leaf pairs",
|
|
5
|
+
"files": [
|
|
6
|
+
"src"
|
|
7
|
+
],
|
|
4
8
|
"type": "module",
|
|
5
9
|
"exports": {
|
|
6
10
|
".": "./src/index.ts",
|
|
7
11
|
"./*": "./src/*"
|
|
8
12
|
},
|
|
13
|
+
"publishConfig": {
|
|
14
|
+
"access": "public"
|
|
15
|
+
},
|
|
9
16
|
"dependencies": {
|
|
10
17
|
"@nativescript/biometrics": "1.3.1",
|
|
18
|
+
"@nativescript/geolocation": "9.0.0",
|
|
11
19
|
"@nativescript/haptics": "3.1.0",
|
|
12
20
|
"@nativescript/imagepicker": "5.0.0",
|
|
13
21
|
"@nativescript/local-notifications": "7.0.0",
|
|
22
|
+
"@nativescript-community/ui-document-picker": "1.1.29",
|
|
14
23
|
"@nativescript/secure-storage": "4.0.2",
|
|
15
24
|
"@nativescript/social-share": "2.3.0",
|
|
16
25
|
"nativescript-clipboard": "2.1.1"
|
|
17
26
|
},
|
|
18
27
|
"devDependencies": {
|
|
28
|
+
"@nativescript-community/octane": "0.2.1",
|
|
19
29
|
"@nativescript/core": "9.1.2",
|
|
20
|
-
"octane": "0.
|
|
30
|
+
"octane": "0.5.0"
|
|
21
31
|
},
|
|
22
32
|
"peerDependencies": {
|
|
33
|
+
"@nativescript-community/octane": ">=0.2.1 <1",
|
|
34
|
+
"@nativescript/core": ">=9.1.0 <10",
|
|
23
35
|
"octane": ">=0.1.51 <1"
|
|
24
36
|
},
|
|
25
|
-
"
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
37
|
+
"peerDependenciesMeta": {
|
|
38
|
+
"@nativescript-community/octane": {
|
|
39
|
+
"optional": true
|
|
40
|
+
},
|
|
41
|
+
"@nativescript/core": {
|
|
42
|
+
"optional": true
|
|
43
|
+
}
|
|
31
44
|
}
|
|
32
45
|
}
|
package/src/a11y.native.ts
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
// A11y announce — native leaf. UIAccessibility announcement notification on
|
|
2
2
|
// iOS; View.announceForAccessibility on Android (posted on the content view).
|
|
3
|
-
import { Application } from '@nativescript/core'
|
|
3
|
+
import { Application } from '@nativescript/core'
|
|
4
4
|
|
|
5
5
|
export function announce(text: string): void {
|
|
6
6
|
if (Application.ios) {
|
|
7
|
-
UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, text)
|
|
8
|
-
return
|
|
7
|
+
UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, text)
|
|
8
|
+
return
|
|
9
9
|
}
|
|
10
|
+
|
|
10
11
|
if (Application.android) {
|
|
11
|
-
const view = Application.android.foregroundActivity?.findViewById?.(
|
|
12
|
-
|
|
12
|
+
const view = Application.android.foregroundActivity?.findViewById?.(
|
|
13
|
+
16908290 /* android.R.id.content */,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
view?.announceForAccessibility?.(text)
|
|
13
17
|
}
|
|
14
18
|
}
|
package/src/a11y.web.ts
CHANGED
|
@@ -1,19 +1,23 @@
|
|
|
1
1
|
// A11y announce — web leaf. A polite aria-live region receives the text;
|
|
2
2
|
// screen readers speak the DOM mutation.
|
|
3
|
-
let region: HTMLElement | null = null
|
|
3
|
+
let region: HTMLElement | null = null
|
|
4
4
|
|
|
5
5
|
export function announce(text: string): void {
|
|
6
6
|
if (!region) {
|
|
7
|
-
region = document.createElement('div')
|
|
8
|
-
region.setAttribute('aria-live', 'polite')
|
|
9
|
-
region.setAttribute('role', 'status')
|
|
7
|
+
region = document.createElement('div')
|
|
8
|
+
region.setAttribute('aria-live', 'polite')
|
|
9
|
+
region.setAttribute('role', 'status')
|
|
10
10
|
region.style.cssText =
|
|
11
|
-
'position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);'
|
|
12
|
-
|
|
11
|
+
'position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);'
|
|
12
|
+
|
|
13
|
+
document.body.appendChild(region)
|
|
13
14
|
}
|
|
14
|
-
|
|
15
|
+
|
|
16
|
+
region.textContent = ''
|
|
15
17
|
// Two writes in one frame collapse — force the SR to see a change.
|
|
16
18
|
requestAnimationFrame(() => {
|
|
17
|
-
if (region)
|
|
18
|
-
|
|
19
|
+
if (region) {
|
|
20
|
+
region.textContent = text
|
|
21
|
+
}
|
|
22
|
+
})
|
|
19
23
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// App info — native leaf. Read the values from the installed app bundle.
|
|
2
|
+
import { Application } from '@nativescript/core'
|
|
3
|
+
import type { AppInfo } from './types'
|
|
4
|
+
|
|
5
|
+
function value(value: unknown): string | null {
|
|
6
|
+
return value === undefined || value === null ? null : String(value)
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function read(): AppInfo {
|
|
10
|
+
if (Application.android) {
|
|
11
|
+
const context = Application.android.context
|
|
12
|
+
const packageId = context.getPackageName()
|
|
13
|
+
const packageInfo = context.getPackageManager().getPackageInfo(packageId, 0)
|
|
14
|
+
return {
|
|
15
|
+
supported: true,
|
|
16
|
+
version: value(packageInfo.versionName),
|
|
17
|
+
build: value(packageInfo.versionCode),
|
|
18
|
+
bundleId: packageId,
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (Application.ios) {
|
|
23
|
+
const bundle = NSBundle.mainBundle
|
|
24
|
+
return {
|
|
25
|
+
supported: true,
|
|
26
|
+
version: value(bundle.objectForInfoDictionaryKey('CFBundleShortVersionString')),
|
|
27
|
+
build: value(bundle.objectForInfoDictionaryKey('CFBundleVersion')),
|
|
28
|
+
bundleId: value(bundle.bundleIdentifier),
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return { supported: false, version: null, build: null, bundleId: null }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const appInfo = read()
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// App info — web leaf. A browser bundle has no trustworthy native version,
|
|
2
|
+
// build, or bundle identifier, so the contract reports that honestly.
|
|
3
|
+
import type { AppInfo } from './types'
|
|
4
|
+
|
|
5
|
+
export const appInfo: AppInfo = {
|
|
6
|
+
supported: false,
|
|
7
|
+
version: null,
|
|
8
|
+
build: null,
|
|
9
|
+
bundleId: null,
|
|
10
|
+
}
|
package/src/biometrics.native.ts
CHANGED
|
@@ -1,24 +1,24 @@
|
|
|
1
1
|
// Biometrics — FaceID/TouchID/fingerprint via @nativescript/biometrics.
|
|
2
|
-
import { BiometricAuth } from '@nativescript/biometrics'
|
|
3
|
-
import type { Capability } from './types'
|
|
4
|
-
import type { BiometricsImpl } from './types'
|
|
2
|
+
import { BiometricAuth } from '@nativescript/biometrics'
|
|
3
|
+
import type { Capability } from './types'
|
|
4
|
+
import type { BiometricsImpl } from './types'
|
|
5
5
|
|
|
6
|
-
const bio = new BiometricAuth()
|
|
6
|
+
const bio = new BiometricAuth()
|
|
7
7
|
|
|
8
8
|
export const biometrics: Capability<BiometricsImpl> = {
|
|
9
9
|
supported: true,
|
|
10
10
|
async ensure() {
|
|
11
|
-
const r = await bio.available()
|
|
12
|
-
return r.any ? 'granted' : 'unsupported'
|
|
11
|
+
const r = await bio.available()
|
|
12
|
+
return r.any ? 'granted' : 'unsupported'
|
|
13
13
|
},
|
|
14
14
|
impl: {
|
|
15
15
|
async verify(reason) {
|
|
16
16
|
try {
|
|
17
|
-
const r = await bio.verifyBiometric({ title: reason })
|
|
18
|
-
return r.code === 0
|
|
17
|
+
const r = await bio.verifyBiometric({ title: reason })
|
|
18
|
+
return r.code === 0
|
|
19
19
|
} catch {
|
|
20
|
-
return false
|
|
20
|
+
return false
|
|
21
21
|
}
|
|
22
22
|
},
|
|
23
23
|
},
|
|
24
|
-
}
|
|
24
|
+
}
|
package/src/biometrics.web.ts
CHANGED
|
@@ -1,21 +1,13 @@
|
|
|
1
|
-
// Biometrics — web leaf.
|
|
2
|
-
//
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
// Biometrics — web leaf. WebAuthn is an authentication ceremony, not a
|
|
2
|
+
// standalone local-presence API: it requires an RP challenge and a registered
|
|
3
|
+
// credential, and may require a user gesture. Do not claim it implements the
|
|
4
|
+
// native verify(reason) contract by creating an ephemeral passkey.
|
|
5
|
+
import type { BiometricsImpl, Capability } from './types'
|
|
6
6
|
|
|
7
7
|
export const biometrics: Capability<BiometricsImpl> = {
|
|
8
|
-
supported:
|
|
8
|
+
supported: false,
|
|
9
9
|
async ensure() {
|
|
10
|
-
|
|
11
|
-
const ok = await pkc().isUserVerifyingPlatformAuthenticatorAvailable();
|
|
12
|
-
return ok ? 'granted' : 'unsupported';
|
|
13
|
-
},
|
|
14
|
-
impl: {
|
|
15
|
-
// A bare verify() has no ceremony target — WebAuthn needs a challenge.
|
|
16
|
-
// Report presence; wire real attestation when an auth flow exists.
|
|
17
|
-
async verify(_reason: string) {
|
|
18
|
-
return false;
|
|
19
|
-
},
|
|
10
|
+
return 'unsupported'
|
|
20
11
|
},
|
|
21
|
-
|
|
12
|
+
impl: null,
|
|
13
|
+
}
|
package/src/clipboard.native.ts
CHANGED
|
@@ -1,17 +1,30 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
import {
|
|
1
|
+
// Core provides native clipboard writes. The plugin remains for reading text,
|
|
2
|
+
// which Core does not expose.
|
|
3
|
+
import { Utils } from '@nativescript/core'
|
|
4
|
+
import { getTextSync } from 'nativescript-clipboard'
|
|
5
|
+
|
|
6
|
+
async function writeText(text: string): Promise<boolean> {
|
|
7
|
+
try {
|
|
8
|
+
Utils.copyToClipboard(text)
|
|
9
|
+
return true
|
|
10
|
+
} catch {
|
|
11
|
+
return false
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function readText(): Promise<string | null> {
|
|
16
|
+
try {
|
|
17
|
+
return getTextSync()
|
|
18
|
+
} catch {
|
|
19
|
+
return null
|
|
20
|
+
}
|
|
21
|
+
}
|
|
4
22
|
|
|
5
23
|
export const clipboard = {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
} catch {
|
|
14
|
-
return null;
|
|
15
|
-
}
|
|
16
|
-
},
|
|
17
|
-
};
|
|
24
|
+
canCopy: true,
|
|
25
|
+
writeText,
|
|
26
|
+
readText,
|
|
27
|
+
// Keep the original names available for existing consumers.
|
|
28
|
+
write: writeText,
|
|
29
|
+
read: readText,
|
|
30
|
+
}
|
package/src/clipboard.web.ts
CHANGED
|
@@ -1,18 +1,30 @@
|
|
|
1
|
-
// Clipboard —
|
|
1
|
+
// Clipboard — browser support is feature-detected; writes can still be denied
|
|
2
|
+
// by browser policy or permission state.
|
|
3
|
+
const canCopy =
|
|
4
|
+
typeof navigator !== 'undefined' && typeof navigator.clipboard?.writeText === 'function'
|
|
5
|
+
|
|
6
|
+
async function writeText(text: string): Promise<boolean> {
|
|
7
|
+
try {
|
|
8
|
+
await navigator.clipboard.writeText(text)
|
|
9
|
+
return true
|
|
10
|
+
} catch {
|
|
11
|
+
return false
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function readText(): Promise<string | null> {
|
|
16
|
+
try {
|
|
17
|
+
return await navigator.clipboard.readText()
|
|
18
|
+
} catch {
|
|
19
|
+
return null
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
2
23
|
export const clipboard = {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
},
|
|
11
|
-
async read(): Promise<string | null> {
|
|
12
|
-
try {
|
|
13
|
-
return await navigator.clipboard.readText();
|
|
14
|
-
} catch {
|
|
15
|
-
return null;
|
|
16
|
-
}
|
|
17
|
-
},
|
|
18
|
-
};
|
|
24
|
+
canCopy,
|
|
25
|
+
writeText,
|
|
26
|
+
readText,
|
|
27
|
+
// Keep the original names available for existing consumers.
|
|
28
|
+
write: writeText,
|
|
29
|
+
read: readText,
|
|
30
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// Connectivity — native leaf. NativeScript exposes this through @nativescript/core;
|
|
2
|
+
// it is a built-in service rather than a separate @nativescript/connectivity plugin.
|
|
3
|
+
import { Connectivity } from '@nativescript/core'
|
|
4
|
+
import type { ConnectionType, ConnectivityImpl, ConnectivityState } from './types'
|
|
5
|
+
|
|
6
|
+
const listeners = new Set<(state: ConnectivityState) => void>()
|
|
7
|
+
let monitoring = false
|
|
8
|
+
|
|
9
|
+
function connectionType(value = Connectivity.getConnectionType()): ConnectionType {
|
|
10
|
+
const types = Connectivity.connectionType as any
|
|
11
|
+
if (value === types.none) {
|
|
12
|
+
return 'none'
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (value === types.wifi) {
|
|
16
|
+
return 'wifi'
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (value === types.mobile) {
|
|
20
|
+
return 'mobile'
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (value === types.ethernet) {
|
|
24
|
+
return 'ethernet'
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (value === types.bluetooth) {
|
|
28
|
+
return 'bluetooth'
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (value === types.vpn) {
|
|
32
|
+
return 'vpn'
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return 'unknown'
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function read(): ConnectivityState {
|
|
39
|
+
const type = connectionType()
|
|
40
|
+
return { online: type !== 'none', type }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function notify(value: number) {
|
|
44
|
+
if (listeners.size === 0) {
|
|
45
|
+
return
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const type = connectionType(value)
|
|
49
|
+
const state = { online: type !== 'none', type }
|
|
50
|
+
for (const listener of listeners) {
|
|
51
|
+
listener(state)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function startMonitoring() {
|
|
56
|
+
if (monitoring) {
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
Connectivity.startMonitoring(notify)
|
|
61
|
+
monitoring = true
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function stopMonitoring() {
|
|
65
|
+
if (!monitoring) {
|
|
66
|
+
return
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
Connectivity.stopMonitoring()
|
|
70
|
+
monitoring = false
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export const connectivity: ConnectivityImpl = {
|
|
74
|
+
getState: read,
|
|
75
|
+
subscribe(listener) {
|
|
76
|
+
listeners.add(listener)
|
|
77
|
+
listener(read())
|
|
78
|
+
startMonitoring()
|
|
79
|
+
return () => {
|
|
80
|
+
listeners.delete(listener)
|
|
81
|
+
if (listeners.size === 0) {
|
|
82
|
+
stopMonitoring()
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// Connectivity — web leaf. Online state comes from navigator.onLine and
|
|
2
|
+
// changes are delivered through the browser's online/offline events.
|
|
3
|
+
import type { ConnectionType, ConnectivityImpl, ConnectivityState } from './types'
|
|
4
|
+
|
|
5
|
+
const listeners = new Set<(state: ConnectivityState) => void>()
|
|
6
|
+
|
|
7
|
+
function connectionType(): ConnectionType {
|
|
8
|
+
const type = (navigator as Navigator & { connection?: { type?: string } }).connection?.type
|
|
9
|
+
switch (type) {
|
|
10
|
+
case 'wifi':
|
|
11
|
+
return 'wifi'
|
|
12
|
+
case 'cellular':
|
|
13
|
+
return 'mobile'
|
|
14
|
+
case 'ethernet':
|
|
15
|
+
return 'ethernet'
|
|
16
|
+
case 'bluetooth':
|
|
17
|
+
return 'bluetooth'
|
|
18
|
+
case 'vpn':
|
|
19
|
+
return 'vpn'
|
|
20
|
+
default:
|
|
21
|
+
return navigator.onLine ? 'unknown' : 'none'
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function read(): ConnectivityState {
|
|
26
|
+
return { online: navigator.onLine, type: connectionType() }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function notify() {
|
|
30
|
+
const state = read()
|
|
31
|
+
for (const listener of listeners) {
|
|
32
|
+
listener(state)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function addListeners() {
|
|
37
|
+
window.addEventListener('online', notify)
|
|
38
|
+
window.addEventListener('offline', notify)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function removeListeners() {
|
|
42
|
+
window.removeEventListener('online', notify)
|
|
43
|
+
window.removeEventListener('offline', notify)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const connectivity: ConnectivityImpl = {
|
|
47
|
+
getState: read,
|
|
48
|
+
subscribe(listener) {
|
|
49
|
+
if (listeners.size === 0) {
|
|
50
|
+
addListeners()
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
listeners.add(listener)
|
|
54
|
+
listener(read())
|
|
55
|
+
return () => {
|
|
56
|
+
listeners.delete(listener)
|
|
57
|
+
if (listeners.size === 0) {
|
|
58
|
+
removeListeners()
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
}
|
package/src/deep-links.native.ts
CHANGED
|
@@ -1,39 +1,66 @@
|
|
|
1
1
|
// Deep links — native leaf. iOS delivers openUrl + launchOptions; Android
|
|
2
2
|
// delivers an intent on resume/newIntent. Handlers get the raw URL string —
|
|
3
3
|
// the app maps it onto its route table.
|
|
4
|
-
import { Application } from '@nativescript/core'
|
|
4
|
+
import { Application } from '@nativescript/core'
|
|
5
5
|
|
|
6
|
-
type LinkHandler = (url: string) => void
|
|
7
|
-
const handlers = new Set<LinkHandler>()
|
|
8
|
-
let wired = false
|
|
9
|
-
let initial: string | null = null
|
|
6
|
+
type LinkHandler = (url: string) => void
|
|
7
|
+
const handlers = new Set<LinkHandler>()
|
|
8
|
+
let wired = false
|
|
9
|
+
let initial: string | null = null
|
|
10
|
+
let lastUrl: string | null = null
|
|
11
|
+
|
|
12
|
+
function dispatch(url: string | null | undefined) {
|
|
13
|
+
if (!url || url === lastUrl) {
|
|
14
|
+
return
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
lastUrl = url
|
|
18
|
+
for (const handler of handlers) {
|
|
19
|
+
handler(url)
|
|
20
|
+
}
|
|
21
|
+
}
|
|
10
22
|
|
|
11
23
|
function wire() {
|
|
12
|
-
if (wired)
|
|
13
|
-
|
|
24
|
+
if (wired) {
|
|
25
|
+
return
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
wired = true
|
|
29
|
+
Application.on(Application.launchEvent, (args: any) => {
|
|
30
|
+
const androidUrl = args.android?.getDataString?.()
|
|
31
|
+
const iosUrl = args.ios?.objectForKey?.('UIApplicationLaunchOptionsURLKey')?.absoluteString
|
|
32
|
+
initial = androidUrl || iosUrl || initial
|
|
33
|
+
dispatch(initial)
|
|
34
|
+
})
|
|
35
|
+
|
|
14
36
|
if (Application.ios) {
|
|
15
37
|
Application.on('openUrl', (args: any) => {
|
|
16
|
-
const url = args.url?.absoluteString ?? String(args.url ?? '')
|
|
17
|
-
|
|
18
|
-
})
|
|
38
|
+
const url = args.url?.absoluteString ?? String(args.url ?? '')
|
|
39
|
+
dispatch(url)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
Application.on('continueActivity', (args: any) => {
|
|
43
|
+
dispatch(args.activity?.webpageURL?.absoluteString)
|
|
44
|
+
})
|
|
19
45
|
}
|
|
46
|
+
|
|
20
47
|
if (Application.android) {
|
|
21
48
|
Application.on(Application.resumeEvent, () => {
|
|
22
|
-
const intent = Application.android.foregroundActivity?.getIntent?.()
|
|
23
|
-
const url = intent?.getDataString?.()
|
|
24
|
-
|
|
25
|
-
})
|
|
49
|
+
const intent = Application.android.foregroundActivity?.getIntent?.()
|
|
50
|
+
const url = intent?.getDataString?.()
|
|
51
|
+
dispatch(url)
|
|
52
|
+
})
|
|
26
53
|
}
|
|
27
54
|
}
|
|
28
55
|
|
|
29
56
|
export function onDeepLink(cb: LinkHandler): () => void {
|
|
30
|
-
wire()
|
|
31
|
-
handlers.add(cb)
|
|
32
|
-
return () => handlers.delete(cb)
|
|
57
|
+
wire()
|
|
58
|
+
handlers.add(cb)
|
|
59
|
+
return () => handlers.delete(cb)
|
|
33
60
|
}
|
|
34
61
|
|
|
35
62
|
export function consumeInitialUrl(): string | null {
|
|
36
|
-
const u = initial
|
|
37
|
-
initial = null
|
|
38
|
-
return u
|
|
63
|
+
const u = initial
|
|
64
|
+
initial = null
|
|
65
|
+
return u
|
|
39
66
|
}
|
package/src/deep-links.web.ts
CHANGED
|
@@ -1,23 +1,28 @@
|
|
|
1
1
|
// Deep links — web leaf. The URL IS the link: consumeInitialUrl returns the
|
|
2
2
|
// current path; onDeepLink listens for popstate (back/forward = link events).
|
|
3
|
-
type LinkHandler = (url: string) => void
|
|
4
|
-
const handlers = new Set<LinkHandler>()
|
|
5
|
-
let wired = false
|
|
3
|
+
type LinkHandler = (url: string) => void
|
|
4
|
+
const handlers = new Set<LinkHandler>()
|
|
5
|
+
let wired = false
|
|
6
6
|
|
|
7
7
|
function wire() {
|
|
8
|
-
if (wired)
|
|
9
|
-
|
|
8
|
+
if (wired) {
|
|
9
|
+
return
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
wired = true
|
|
10
13
|
window.addEventListener('popstate', () => {
|
|
11
|
-
for (const h of handlers)
|
|
12
|
-
|
|
14
|
+
for (const h of handlers) {
|
|
15
|
+
h(location.pathname + location.search)
|
|
16
|
+
}
|
|
17
|
+
})
|
|
13
18
|
}
|
|
14
19
|
|
|
15
20
|
export function onDeepLink(cb: LinkHandler): () => void {
|
|
16
|
-
wire()
|
|
17
|
-
handlers.add(cb)
|
|
18
|
-
return () => handlers.delete(cb)
|
|
21
|
+
wire()
|
|
22
|
+
handlers.add(cb)
|
|
23
|
+
return () => handlers.delete(cb)
|
|
19
24
|
}
|
|
20
25
|
|
|
21
26
|
export function consumeInitialUrl(): string | null {
|
|
22
|
-
return location.pathname === '/' ? null : location.pathname + location.search
|
|
27
|
+
return location.pathname === '/' ? null : location.pathname + location.search
|
|
23
28
|
}
|
package/src/device.native.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Device } from '@nativescript/core'
|
|
2
|
-
import type { DeviceInfo } from './types'
|
|
1
|
+
import { Device } from '@nativescript/core'
|
|
2
|
+
import type { DeviceInfo } from './types'
|
|
3
3
|
|
|
4
4
|
export const device: DeviceInfo = {
|
|
5
5
|
os: Device.os.toLowerCase() === 'ios' ? 'ios' : 'android',
|
|
@@ -8,4 +8,4 @@ export const device: DeviceInfo = {
|
|
|
8
8
|
manufacturer: Device.manufacturer,
|
|
9
9
|
language: Device.language.split('-')[0] ?? '',
|
|
10
10
|
region: Device.region,
|
|
11
|
-
}
|
|
11
|
+
}
|
package/src/device.web.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import type { DeviceInfo } from './types'
|
|
1
|
+
import type { DeviceInfo } from './types'
|
|
2
2
|
|
|
3
|
-
const ua = navigator.userAgent
|
|
4
|
-
const osVersion =
|
|
3
|
+
const ua = navigator.userAgent
|
|
4
|
+
const osVersion =
|
|
5
|
+
ua.match(/(?:Mac OS X|Windows NT|Android|OS) ([\d._]+)/)?.[1]?.replace(/_/g, '.') ?? ''
|
|
5
6
|
|
|
6
7
|
export const device: DeviceInfo = {
|
|
7
8
|
os: 'web',
|
|
@@ -10,4 +11,4 @@ export const device: DeviceInfo = {
|
|
|
10
11
|
manufacturer: '',
|
|
11
12
|
language: navigator.language.split('-')[0] ?? '',
|
|
12
13
|
region: navigator.language.split('-')[1] ?? '',
|
|
13
|
-
}
|
|
14
|
+
}
|