@de./sdk 2.1.0 → 2.1.1

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.
@@ -1,13 +1,17 @@
1
+ import type { SigninBody, CreateAccountBody } from '@de./types';
1
2
  import type { AccessOptions, UserSession } from '../../types/access';
2
3
  import AccessManager from '../Access';
3
4
  import { type Res } from '../../utils';
4
- export type SigninBody = {
5
- phone: string;
6
- /** Device fingerprint — an object or its JSON string. */
7
- device: string | Record<string, any>;
8
- /** ISO 3166-1 alpha-2. */
9
- country?: string;
10
- };
5
+ /**
6
+ * Request bodies come from `@de./types`, which is what de.auth validates
7
+ * against — these were hand-declared here, and said `country?: string` where
8
+ * de.auth wants the Country OBJECT. Its schema is additionalProperties:false,
9
+ * so a caller trusting the SDK's type was answered `body/country must be
10
+ * object`. Nothing caught it because this client could not be constructed at
11
+ * all: AccessManager demanded a workspace context and a bearer token, which
12
+ * sign-in is what produces.
13
+ */
14
+ export type { SigninBody, CreateAccountBody, VerificationBody, Country } from '@de./types';
11
15
  export type SigninResult = {
12
16
  /** What the caller must do next — verification, or account creation first. */
13
17
  next: string;
@@ -19,16 +23,6 @@ export type SigninResult = {
19
23
  */
20
24
  testVCode?: number;
21
25
  };
22
- export type CreateAccountBody = {
23
- phone: string;
24
- agreeTerms: boolean | string;
25
- firstName?: string;
26
- lastName?: string;
27
- photo?: string;
28
- type?: string;
29
- country?: string;
30
- newsletters?: boolean | string;
31
- };
32
26
  export type VerifyResult = {
33
27
  ctoken?: string;
34
28
  deviceId?: string;
@@ -51,7 +45,7 @@ export default class OTPAuth extends AccessManager {
51
45
  resendSms(body: {
52
46
  phone: string;
53
47
  }): Promise<Res<void>>;
54
- /** GET, with the scope in the querystring — not a POST. */
48
+ /** A state change, so a POST — it was a GET, which is cacheable. */
55
49
  signout(querystring?: {
56
50
  allDevices?: boolean | string;
57
51
  }): Promise<Res<void>>;
@@ -7,29 +7,39 @@ const Access_1 = __importDefault(require("../Access"));
7
7
  const utils_1 = require("../../utils");
8
8
  class OTPAuth extends Access_1.default {
9
9
  constructor(access) {
10
- super(access, 'ASI');
10
+ /**
11
+ * de.auth admits an agent that does not start with `De.service` only if
12
+ * the request carries an Authorization header — and the whole point of
13
+ * this client is the flow that has no token yet. The SDK's default
14
+ * platform is `proxy`, so every sign-in through it was answered 412
15
+ * before reaching a handler.
16
+ *
17
+ * `service` is the platform this client is for; a caller that knows
18
+ * better can still say so.
19
+ */
20
+ super({ ...access, platform: access.platform || 'service' }, 'ASI');
11
21
  }
12
22
  // ── Sign-in ───────────────────────────────────────────────────────────────
13
23
  /** Step 1 — sends the code. */
14
24
  async signin(body) {
15
25
  if (!body.phone)
16
26
  throw new Error('<phone> argument required');
17
- return this.request({ url: '/signin', method: 'POST', body });
27
+ return this.request({ url: '/auth/otp/send', method: 'POST', body });
18
28
  }
19
29
  /** Step 2 — exchanges the code for a session. */
20
30
  async verify(body) {
21
- return this.request({ url: '/verification', method: 'POST', body });
31
+ return this.request({ url: '/auth/otp/verify', method: 'POST', body });
22
32
  }
23
33
  /** Step 1a — for a phone de.auth has never seen. */
24
34
  async setAccount(body) {
25
- return this.request({ url: '/set-account', method: 'POST', body });
35
+ return this.request({ url: '/auth/account/create', method: 'POST', body });
26
36
  }
27
37
  async resendSms(body) {
28
- return this.request({ url: '/resend/sms', method: 'POST', body });
38
+ return this.request({ url: '/auth/otp/resend', method: 'POST', body });
29
39
  }
30
- /** GET, with the scope in the querystring — not a POST. */
40
+ /** A state change, so a POST — it was a GET, which is cacheable. */
31
41
  async signout(querystring) {
32
- return this.request({ url: `/signout${(0, utils_1.qs)(querystring)}`, method: 'GET' });
42
+ return this.request({ url: `/auth/session/revoke${(0, utils_1.qs)(querystring)}`, method: 'POST' });
33
43
  }
34
44
  // ── The signed-in person ──────────────────────────────────────────────────
35
45
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/allend/OTPAuth/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/allend/OTPAuth/index.ts"],"names":[],"mappings":"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"}
@@ -23,6 +23,20 @@ export type Res<T = any> = {
23
23
  * property fails that test outright.
24
24
  */
25
25
  export type Data<T> = 'data' extends keyof T ? T['data'] : undefined;
26
+ /**
27
+ * Serialize a querystring for a De. route.
28
+ *
29
+ * Array values are appended as repeated keys — `state=A&state=B` — which is
30
+ * what Fastify's query parser turns back into an array. `String( value )` on
31
+ * an array produces `A,B`, a single comma-joined string, and the route answers
32
+ * `querystring/state must be array`: the schema is right, the request never
33
+ * matched it. Every array parameter the SDK sent was malformed this way, the
34
+ * same shape of fault as the coordinates object that once serialized to
35
+ * `[object Object]`.
36
+ *
37
+ * @param params - Query parameters; null and undefined values are dropped
38
+ * @return - `?a=1&b=2`, or an empty string when there is nothing to send
39
+ */
26
40
  export declare const qs: (params?: Record<string, any>) => string;
27
41
  declare const _default: {
28
42
  Stream: typeof Stream;
@@ -5,13 +5,33 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.qs = void 0;
7
7
  const stream_1 = __importDefault(require("./stream"));
8
+ /**
9
+ * Serialize a querystring for a De. route.
10
+ *
11
+ * Array values are appended as repeated keys — `state=A&state=B` — which is
12
+ * what Fastify's query parser turns back into an array. `String( value )` on
13
+ * an array produces `A,B`, a single comma-joined string, and the route answers
14
+ * `querystring/state must be array`: the schema is right, the request never
15
+ * matched it. Every array parameter the SDK sent was malformed this way, the
16
+ * same shape of fault as the coordinates object that once serialized to
17
+ * `[object Object]`.
18
+ *
19
+ * @param params - Query parameters; null and undefined values are dropped
20
+ * @return - `?a=1&b=2`, or an empty string when there is nothing to send
21
+ */
8
22
  const qs = (params) => {
9
23
  if (!params)
10
24
  return '';
11
- const q = new URLSearchParams(Object.fromEntries(Object.entries(params)
12
- .filter(([, v]) => v != null)
13
- .map(([k, v]) => [k, String(v)]))).toString();
14
- return q ? `?${q}` : '';
25
+ const q = new URLSearchParams();
26
+ for (const [key, value] of Object.entries(params)) {
27
+ if (value === null || value === undefined)
28
+ continue;
29
+ Array.isArray(value)
30
+ ? value.forEach(each => each !== null && each !== undefined && q.append(key, String(each)))
31
+ : q.append(key, String(value));
32
+ }
33
+ const query = q.toString();
34
+ return query ? `?${query}` : '';
15
35
  };
16
36
  exports.qs = qs;
17
37
  exports.default = { Stream: stream_1.default };
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":";;;;;;AACA,sDAA6B;AAsBtB,MAAM,EAAE,GAAG,CAAE,MAA4B,EAAW,EAAE;IAC3D,IAAI,CAAC,MAAM;QAAG,OAAO,EAAE,CAAA;IACvB,MAAM,CAAC,GAAG,IAAI,eAAe,CAC3B,MAAM,CAAC,WAAW,CAChB,MAAM,CAAC,OAAO,CAAE,MAAM,CAAE;SACjB,MAAM,CAAE,CAAE,CAAC,EAAE,CAAC,CAAC,EAAG,EAAE,CAAC,CAAC,IAAI,IAAI,CAAE;SAChC,GAAG,CAAE,CAAE,CAAC,CAAC,EAAE,CAAC,CAAC,EAAG,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,CAAE,CAAC,CAAE,CAAC,CAAE,CAC7C,CACF,CAAC,QAAQ,EAAE,CAAA;IACZ,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAA;AACzB,CAAC,CAAA;AAVY,QAAA,EAAE,MAUd;AAED,kBAAe,EAAE,MAAM,EAAN,gBAAM,EAAE,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":";;;;;;AACA,sDAA6B;AAsB7B;;;;;;;;;;;;;GAaG;AACI,MAAM,EAAE,GAAG,CAAE,MAA4B,EAAW,EAAE;IAC3D,IAAI,CAAC,MAAM;QAAG,OAAO,EAAE,CAAA;IAEvB,MAAM,CAAC,GAAG,IAAI,eAAe,EAAE,CAAA;IAE/B,KAAK,MAAM,CAAE,GAAG,EAAE,KAAK,CAAE,IAAI,MAAM,CAAC,OAAO,CAAE,MAAM,CAAE,EAAE,CAAC;QACtD,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,SAAS;YAAG,SAAQ;QAEpD,KAAK,CAAC,OAAO,CAAE,KAAK,CAAE;YACpB,CAAC,CAAC,KAAK,CAAC,OAAO,CAAE,IAAI,CAAC,EAAE,CAAC,IAAI,KAAK,IAAI,IAAI,IAAI,KAAK,SAAS,IAAI,CAAC,CAAC,MAAM,CAAE,GAAG,EAAE,MAAM,CAAE,IAAI,CAAE,CAAE,CAAE;YACjG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAE,GAAG,EAAE,MAAM,CAAE,KAAK,CAAE,CAAE,CAAA;IACtC,CAAC;IAED,MAAM,KAAK,GAAG,CAAC,CAAC,QAAQ,EAAE,CAAA;IAC1B,OAAO,KAAK,CAAC,CAAC,CAAC,IAAI,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAA;AACjC,CAAC,CAAA;AAfY,QAAA,EAAE,MAed;AAED,kBAAe,EAAE,MAAM,EAAN,gBAAM,EAAE,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@de./sdk",
3
- "version": "2.1.0",
3
+ "version": "2.1.1",
4
4
  "description": "Complete SDK pack for MSI (Map Service Interface), IoTClient and DClient services — web, server and React Native",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -43,10 +43,10 @@
43
43
  "dependencies": {
44
44
  "@de./types": "^2.1.0",
45
45
  "events": "^3.3.0",
46
- "iframe.io": "^1.1.0",
46
+ "iframe.io": "^1.3.0",
47
47
  "node-fetch": "2.6.1",
48
48
  "socket.io-client": "^4.6.1",
49
- "webview.io": "^1.1.0"
49
+ "webview.io": "^1.2.0"
50
50
  },
51
51
  "peerDependencies": {
52
52
  "react": ">=16.8.0",
@@ -54,9 +54,15 @@
54
54
  "react-native-webview": ">=11.0.0"
55
55
  },
56
56
  "peerDependenciesMeta": {
57
- "react": { "optional": true },
58
- "react-native": { "optional": true },
59
- "react-native-webview": { "optional": true }
57
+ "react": {
58
+ "optional": true
59
+ },
60
+ "react-native": {
61
+ "optional": true
62
+ },
63
+ "react-native-webview": {
64
+ "optional": true
65
+ }
60
66
  },
61
67
  "devDependencies": {
62
68
  "@babel/core": "^7.26.0",
@@ -56,10 +56,26 @@ export default class AccessManager {
56
56
 
57
57
  constructor( options: AccessOptions, atype: AccessType ){
58
58
  if( !options ) throw new Error('Undefined Access Configuration')
59
- if( !options.context ) throw new Error('Undefined Context Reference. Check https://doc.dedot.io/sdk/auth')
60
- if( !options.accessToken ) throw new Error('Undefined Access Token. Check https://doc.dedot.io/sdk/auth')
61
-
62
- this.context = options.context
59
+
60
+ /**
61
+ * A workspace context and a bearer token identify a CALLER to de.arch, and
62
+ * every API client needs both. de.auth answers a different question — who
63
+ * the PERSON is — and the sign-in flow is what produces a session in the
64
+ * first place: at `POST /auth/otp/send` there is no workspace to name and
65
+ * no token to bear.
66
+ *
67
+ * Requiring them of every access type meant `OTPAuth` threw in its own
68
+ * constructor and could never be used for the one thing it exists to do.
69
+ * That is why de.simulation's bootstrap and Hot's `getUser` both talk to
70
+ * de.auth over raw fetch, and why nothing ever caught that this client
71
+ * addressed `/v1/auth/otp` — a route de.auth has never served.
72
+ */
73
+ if( atype !== 'ASI' ){
74
+ if( !options.context ) throw new Error('Undefined Context Reference. Check https://doc.dedot.io/sdk/auth')
75
+ if( !options.accessToken ) throw new Error('Undefined Access Token. Check https://doc.dedot.io/sdk/auth')
76
+ }
77
+
78
+ this.context = options.context || ''
63
79
  this.atype = atype
64
80
  this.version = options.version || 1
65
81
  this.platform = options.platform || 'proxy'
@@ -5,7 +5,10 @@ import type {
5
5
  LSPAgentUpdateValidation,
6
6
  LSPAgentAssignVehicleValidation,
7
7
  LSPAgentUnassignVehicleValidation,
8
- LSPAgentRemoveValidation
8
+ LSPAgentRemoveValidation,
9
+ LSPAgentEnrolValidation,
10
+ LSPAgentEnrolmentRetrieveValidation,
11
+ LSPAgentEnrolmentUpdateValidation
9
12
  } from '@de./types/aux/agents'
10
13
  import type {
11
14
  LSPConsolidationScheduleValidation,
@@ -22,6 +25,48 @@ import { qs, type Http, type Res, type Data } from '../../utils'
22
25
  export default class LSPAgents {
23
26
  constructor( private http: Http ){}
24
27
 
28
+ // ── Self-enrolment ──────────────────────────────────────────────────────
29
+ //
30
+ // The invitation flow needs an operator to name each agent, which is right
31
+ // when a provider picks riders one at a time and wrong for a platform that
32
+ // already runs its own verification. Enrolment keeps what invitation was
33
+ // buying — the agent lands in this provider's context, and the provider
34
+ // agreed to take them — but takes the agreement from a standing policy
35
+ // instead of a per-person act.
36
+
37
+ /** The provider's standing enrolment policy. Operator-scoped. */
38
+ async enrolmentPolicy(): Promise<LSPAgentEnrolmentRetrieveValidation['response']> {
39
+ return await this.http.request<LSPAgentEnrolmentRetrieveValidation['response']>({
40
+ url: '/lsp/agents/enrolment',
41
+ method: 'GET'
42
+ })
43
+ }
44
+
45
+ /** Set it. Off unless `enabled` says otherwise. Operator-scoped. */
46
+ async setEnrolmentPolicy( body: LSPAgentEnrolmentUpdateValidation['body'] ): Promise<LSPAgentEnrolmentUpdateValidation['response']> {
47
+ return await this.http.request<LSPAgentEnrolmentUpdateValidation['response']>({
48
+ url: '/lsp/agents/enrolment',
49
+ method: 'PUT',
50
+ body
51
+ })
52
+ }
53
+
54
+ /**
55
+ * Join this provider as an agent.
56
+ *
57
+ * Authorized as the person joining — the uid on the token — so a platform
58
+ * enrols on someone's behalf by minting for them, exactly as it would to
59
+ * redeem an invitation. `status`, `grade` and the zone come from the
60
+ * provider's policy, not from the body.
61
+ */
62
+ async enrol( body: LSPAgentEnrolValidation['body'] ): Promise<LSPAgentEnrolValidation['response']> {
63
+ return await this.http.request<LSPAgentEnrolValidation['response']>({
64
+ url: '/lsp/agents/enrolment',
65
+ method: 'POST',
66
+ body
67
+ })
68
+ }
69
+
25
70
  async list( querystring?: LSPAgentListValidation['querystring'] ): Promise<LSPAgentListValidation['response']> {
26
71
  return await this.http.request<LSPAgentListValidation['response']>({
27
72
  url: `/lsp/agents${qs( querystring )}`,
@@ -46,13 +46,21 @@ export default class Handles extends EventEmitter {
46
46
  /**
47
47
  * Create new stream through which user's current location
48
48
  * details will be pushed to the API level.
49
- *
50
- * @param usertype - (Default: `client`) Type of user at the current location
49
+ *
50
+ * This took a `usertype` of `client | agent` and sent it as the payload of
51
+ * `pin:current:location`. The gateway's handler takes an acknowledgement in
52
+ * that position and its `geolocator.pin()` takes no arguments at all, so the
53
+ * value was bound to `callback`, failed the `typeof … == 'function'` test and
54
+ * was discarded — `myLocation('agent')` and `myLocation('client')` did the
55
+ * same thing. `Controls.pinCurrentLocation()` emits the same event correctly,
56
+ * with only the ack. Removed rather than kept as decoration; distinguishing
57
+ * the marker by user type is a gateway feature that does not exist yet.
58
+ *
51
59
  * @return - Readable stream
52
60
  */
53
- myLocation( usertype?: 'client' | 'agent' ){
61
+ myLocation(){
54
62
  if( !this.chn ) return
55
- this.chn?.emit('pin:current:location', usertype || 'client' )
63
+ this.chn?.emit('pin:current:location')
56
64
 
57
65
  const stream = new Stream()
58
66
 
@@ -217,6 +217,26 @@ export default class MSI extends EventEmitter {
217
217
  */
218
218
  isReady(){ return this.chn && this.isConnected }
219
219
 
220
+ /**
221
+ * Tear the gateway down.
222
+ *
223
+ * Every `load()` adds a `message` listener to the window and an iframe to
224
+ * the page, and neither went away: a single-page host that mounts a map on
225
+ * a route accumulated one of each per visit, all of them still parsing
226
+ * traffic from the gateway. The React Native entry already unwinds this on
227
+ * unmount; the web entry had no way to.
228
+ */
229
+ destroy(){
230
+ this.chn?.disconnect()
231
+ this.chn = undefined
232
+ this.isConnected = false
233
+
234
+ const container = document.getElementById( this.element )
235
+ if( container ) container.innerHTML = ''
236
+
237
+ this.removeAllListeners()
238
+ }
239
+
220
240
  /**
221
241
  * Extend the kit's functionalities
222
242
  *
@@ -1,6 +1,6 @@
1
1
  import type { MapOptions } from '../../types'
2
2
 
3
- import React, { useRef, useEffect, useState, useCallback, forwardRef, useImperativeHandle } from 'react'
3
+ import React, { useRef, useEffect, useState, useCallback, useMemo, forwardRef, useImperativeHandle } from 'react'
4
4
  import { View, StyleSheet, AppState, type AppStateStatus, Platform } from 'react-native'
5
5
  import { WebView } from 'react-native-webview'
6
6
  import WIO from 'webview.io'
@@ -8,6 +8,7 @@ import Handles from './Handles'
8
8
  import Controls from './Controls'
9
9
  import Plugins, { type Plugin } from './Plugins'
10
10
  import resolveAccessToken from './token'
11
+ import belongsToGateway from './origin'
11
12
 
12
13
  export interface MSIInterface {
13
14
  controls: Controls
@@ -115,6 +116,18 @@ export default forwardRef<MSIRef, MSIProps>( ( props, ref ) => {
115
116
  apiRef = useRef<MSIInterface | null>( null ),
116
117
  isInitializedRef = useRef( false ),
117
118
 
119
+ /**
120
+ * The bridge is built once, so its handlers close over the props of the
121
+ * render that built it. `bind` resolves the access token at connect time
122
+ * precisely so a reconnect picks up a rotated one -- but reading it from a
123
+ * captured `props` gave back whatever `getAccessToken` closed over at mount,
124
+ * which for the usual `() => token` is the token the host held then. Every
125
+ * reconnect after a rotation would have re-bound with a dead token. Same for
126
+ * the callbacks: a host passing inline handlers had the mount-time ones
127
+ * called forever.
128
+ */
129
+ propsRef = useRef( props ),
130
+
118
131
  [ isConnected, setIsConnected ] = useState( false ),
119
132
  [ isReady, setIsReady ] = useState( false ),
120
133
 
@@ -128,13 +141,46 @@ export default forwardRef<MSIRef, MSIProps>( ( props, ref ) => {
128
141
  ? error
129
142
  : new Error( typeof error === 'string' ? error : error?.error || 'Unknown MSI error')
130
143
 
131
- props.onError?.( normalized )
132
- }, [ props.onError ] ),
144
+ propsRef.current.onError?.( normalized )
145
+ }, [] ),
133
146
 
134
147
  initializeConnection = useCallback( () => {
135
148
  if( !wioRef.current || !webViewRef.current ) return
136
149
  wioRef.current.initiate( webViewRef, baseURL )
137
- }, [ baseURL ] )
150
+ }, [ baseURL ] ),
151
+
152
+ /**
153
+ * Refuse to navigate anywhere but the gateway -- see `belongsToGateway`.
154
+ */
155
+ isGatewayURL = useCallback( ( url: string ) => belongsToGateway( baseURL, url ), [ baseURL ] ),
156
+
157
+ /**
158
+ * One options object for both ends of the bridge.
159
+ *
160
+ * `getInjectedJavaScript()` bakes the transport's own configuration into the
161
+ * script the WebView runs — the auth secret, the skew tolerance, the replay
162
+ * window. It used to be called on a throwaway `new WIO({ type: 'WEBVIEW' })`
163
+ * constructed inline in the JSX, which shares nothing with the instance that
164
+ * actually holds the connection: the embedded side would have been handed a
165
+ * null secret and default tolerances no matter what the host was configured
166
+ * with. Nothing configures cryptoAuth today, so it never showed.
167
+ */
168
+ wioOptions = useMemo( () => ({
169
+ type: 'WEBVIEW' as const,
170
+ debug: props.env === 'dev',
171
+ allowedIncomingEvents: ALLOWED_INCOMING_EVENTS,
172
+ maxMessagesPerSecond: 100,
173
+ connectionTimeout: 15000,
174
+ connectionPingInterval: 2000,
175
+ maxConnectionAttempts: 5,
176
+ autoReconnect: true,
177
+ heartbeatInterval: 30000
178
+ }), [ props.env ] ),
179
+
180
+ // Rebuilt only when those options change, rather than on every render.
181
+ injectedBridge = useMemo( () => new WIO( wioOptions ).getInjectedJavaScript(), [ wioOptions ] )
182
+
183
+ useEffect( () => { propsRef.current = props } )
138
184
 
139
185
  // Expose the control surface to the host
140
186
  useImperativeHandle( ref, () => ({
@@ -149,17 +195,7 @@ export default forwardRef<MSIRef, MSIProps>( ( props, ref ) => {
149
195
  }), [ isConnected, isReady, initializeConnection ] )
150
196
 
151
197
  useEffect( () => {
152
- const wio = wioRef.current = new WIO({
153
- type: 'WEBVIEW',
154
- debug: props.env === 'dev',
155
- allowedIncomingEvents: ALLOWED_INCOMING_EVENTS,
156
- maxMessagesPerSecond: 100,
157
- connectionTimeout: 15000,
158
- connectionPingInterval: 2000,
159
- maxConnectionAttempts: 5,
160
- autoReconnect: true,
161
- heartbeatInterval: 30000
162
- })
198
+ const wio = wioRef.current = new WIO( wioOptions )
163
199
 
164
200
  wio
165
201
  .on('connect', async () => {
@@ -168,11 +204,11 @@ export default forwardRef<MSIRef, MSIProps>( ( props, ref ) => {
168
204
  * Resolve the token here rather than at mount, so a reconnect after a
169
205
  * backgrounded app or a dropped bridge binds with a current token.
170
206
  */
171
- const { getAccessToken, onReady, onError, onLoaded, style, ...config } = props
207
+ const { getAccessToken, onReady, onError, onLoaded, style, ...config } = propsRef.current
172
208
 
173
209
  await wio.emitAsync('bind', {
174
210
  ...config,
175
- accessToken: resolveAccessToken( props ),
211
+ accessToken: resolveAccessToken( propsRef.current ),
176
212
  origin: 'react-native'
177
213
  }, BIND_TIMEOUT )
178
214
 
@@ -182,7 +218,7 @@ export default forwardRef<MSIRef, MSIProps>( ( props, ref ) => {
182
218
  })
183
219
  .on('ready', () => {
184
220
  setIsReady( true )
185
- props.onReady?.()
221
+ propsRef.current.onReady?.()
186
222
  })
187
223
  .on('disconnect', () => {
188
224
  setIsConnected( false )
@@ -201,19 +237,21 @@ export default forwardRef<MSIRef, MSIProps>( ( props, ref ) => {
201
237
  })
202
238
 
203
239
  /**
204
- * Pause gateway work while backgrounded, and re-establish the bridge on
205
- * resume -- iOS in particular tears down the WebView's socket.
240
+ * Re-establish the bridge on resume -- iOS in particular tears down the
241
+ * WebView's socket while the app is backgrounded.
242
+ *
243
+ * This used to also emit `app:background` / `app:foreground`, which read
244
+ * like the gateway paused its work. It never did: the gateway has no
245
+ * handler for either, and its own allowlist dropped them before they got
246
+ * that far. Nor should there be a blanket one -- a rider streaming their
247
+ * position must keep streaming with the phone in their pocket, so whether
248
+ * backgrounding stops anything is the host's call, made through
249
+ * `controls.untrackLiveLocation()` and its counterpart.
206
250
  */
207
251
  const subscription = AppState.addEventListener('change', ( state: AppStateStatus ) => {
208
- if( state === 'background' ){
209
- wio.emit('app:background')
210
- return
211
- }
212
-
213
- if( state === 'active' ){
214
- wio.emit('app:foreground')
215
- !wio.isConnected() && initializeConnection()
216
- }
252
+ state === 'active'
253
+ && !wio.isConnected()
254
+ && initializeConnection()
217
255
  })
218
256
 
219
257
  return () => {
@@ -231,16 +269,16 @@ export default forwardRef<MSIRef, MSIProps>( ( props, ref ) => {
231
269
 
232
270
  const
233
271
  chn = wioRef.current,
234
- controls = new Controls( chn, props ),
235
- handles = new Handles( chn, controls, props ),
236
- plugins = new Plugins( chn, handles, controls, props )
272
+ controls = new Controls( chn, propsRef.current ),
273
+ handles = new Handles( chn, controls, propsRef.current ),
274
+ plugins = new Plugins( chn, handles, controls, propsRef.current )
237
275
 
238
276
  plugins.mount( REGISTERED_PLUGINS )
239
277
 
240
278
  apiRef.current = { controls, handles, plugins }
241
279
  isInitializedRef.current = true
242
280
 
243
- props.onLoaded?.( apiRef.current )
281
+ propsRef.current.onLoaded?.( apiRef.current )
244
282
  }, [ isConnected, isReady ] )
245
283
 
246
284
  const onMessage = ( event: any ) => {
@@ -274,7 +312,7 @@ export default forwardRef<MSIRef, MSIProps>( ( props, ref ) => {
274
312
  style={styles.webview}
275
313
  injectedJavaScript={`
276
314
  ${props.env === 'dev' ? injectedConsole() : ''}
277
- ${new WIO({ type: 'WEBVIEW' }).getInjectedJavaScript()}
315
+ ${injectedBridge}
278
316
  `}
279
317
  javaScriptEnabled
280
318
  domStorageEnabled
@@ -287,6 +325,7 @@ export default forwardRef<MSIRef, MSIProps>( ( props, ref ) => {
287
325
  bounces={false}
288
326
  scrollEnabled={false}
289
327
  originWhitelist={[ baseURL ]}
328
+ onShouldStartLoadWithRequest={( { url }: any ) => isGatewayURL( url )}
290
329
  onMessage={onMessage}
291
330
  onLoadEnd={initializeConnection}
292
331
  onError={( { nativeEvent }: any ) => reportError( new Error( nativeEvent?.description || 'WebView error') )}
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Whether a URL belongs to the MSI gateway itself.
3
+ *
4
+ * React Native's `originWhitelist` is prefix-matched and unanchored at the
5
+ * end, so a whitelist of `https://msi.dedot.io` also admits
6
+ * `https://msi.dedot.io.example.com`. That matters more in a WebView than it
7
+ * would in a browser: the injected bridge — and the auth secret baked into
8
+ * it — runs in whatever page the WebView ends up on. So the origin has to
9
+ * match exactly, and only a path, query or fragment separator may follow it.
10
+ *
11
+ * @param baseURL - Gateway origin, without a trailing slash
12
+ * @param url - URL the WebView is asking to navigate to
13
+ * @return - Whether the navigation is to the gateway
14
+ */
15
+ export default function belongsToGateway( baseURL: string, url: string ): boolean {
16
+ if( !url || !baseURL ) return false
17
+ if( url === 'about:blank' || url === baseURL ) return true
18
+
19
+ return [ '/', '?', '#' ].some( separator => url.startsWith( baseURL + separator ) )
20
+ }