@cloudparse/up-miniapps-sdk 0.5.7 → 0.6.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.
package/dist/index.cjs CHANGED
@@ -50,6 +50,8 @@ __export(src_exports, {
50
50
  SDKProvider: () => SDKProvider,
51
51
  SwipeCarousel: () => SwipeCarousel,
52
52
  UIEvent: () => UIEvent,
53
+ createApiService: () => createApiService,
54
+ createWsService: () => createWsService,
53
55
  default: () => src_default,
54
56
  handleClearRoute: () => handleClearRoute,
55
57
  handleDrawRoute: () => handleDrawRoute,
@@ -124,7 +126,16 @@ var Button = React.forwardRef(
124
126
  );
125
127
  Button.displayName = "Button";
126
128
 
129
+ // src/utils/environment.ts
130
+ function isRunningInsideNativeShell() {
131
+ if (typeof window === "undefined") return false;
132
+ const w = window;
133
+ return w.__UP_REACT_NATIVE_APP__ === true || !!w.ReactNativeWebView;
134
+ }
135
+
127
136
  // src/bridge.ts
137
+ var BridgeTimeoutError = class extends Error {
138
+ };
128
139
  var Bridge = class {
129
140
  callbacks = /* @__PURE__ */ new Map();
130
141
  eventListeners = /* @__PURE__ */ new Map();
@@ -195,18 +206,32 @@ var Bridge = class {
195
206
  * @param type The action type (e.g., "GET_LOCATION").
196
207
  * @param payload Data associated with the action.
197
208
  * @param timeoutMs Time in milliseconds to wait for a response before rejecting.
209
+ * @param retries Number of additional attempts if a request times out (default 1 — one retry).
210
+ * Exists because the response for a request can be lost in transit — e.g. the host's
211
+ * injected response script running before this WebView's own `message` listener has
212
+ * attached, worse under rapid mount/unmount cycles — with no application-visible symptom
213
+ * other than the full 30s timeout. A blind retry self-heals that class of loss without
214
+ * needing to diagnose which specific transport race dropped the original message; every
215
+ * call site benefits automatically instead of each consumer app hand-rolling its own
216
+ * retry-after-catch logic (which previously only covered `initialize()`, and imperfectly).
198
217
  * @returns A promise that resolves with the native response.
199
218
  */
200
- send(type, payload, timeoutMs = 3e4) {
219
+ send(type, payload, timeoutMs = 3e4, retries = 1) {
201
220
  if (!this.isNative()) {
202
221
  return this.handleMockMessage(type, payload);
203
222
  }
223
+ return this.sendOnce(type, payload, timeoutMs).catch((err) => {
224
+ if (retries <= 0 || !(err instanceof BridgeTimeoutError)) throw err;
225
+ return this.send(type, payload, timeoutMs, retries - 1);
226
+ });
227
+ }
228
+ sendOnce(type, payload, timeoutMs) {
204
229
  return new Promise((resolve, reject) => {
205
230
  const callbackId = `${type}_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
206
231
  const timeout = setTimeout(() => {
207
232
  if (this.callbacks.has(callbackId)) {
208
233
  this.callbacks.delete(callbackId);
209
- reject(new Error(`Bridge request timeout: ${type}`));
234
+ reject(new BridgeTimeoutError(`Bridge request timeout: ${type}`));
210
235
  }
211
236
  }, timeoutMs);
212
237
  this.callbacks.set(callbackId, {
@@ -280,8 +305,7 @@ var Bridge = class {
280
305
  * Determine environment. Check flag or cache result.
281
306
  */
282
307
  isNative() {
283
- if (typeof window === "undefined") return false;
284
- return window.__UP_REACT_NATIVE_APP__ === true || !!window.ReactNativeWebView;
308
+ return isRunningInsideNativeShell();
285
309
  }
286
310
  };
287
311
 
@@ -2028,14 +2052,14 @@ var useUserLocation = () => {
2028
2052
  });
2029
2053
  const mapPayload = (0, import_react6.useCallback)((payload) => {
2030
2054
  const coords = payload.location?.coords || payload.coords;
2031
- const headingData = payload.heading || payload.coords;
2055
+ const headingData = payload.heading;
2032
2056
  const newCoords = coords ? [coords.longitude, coords.latitude] : null;
2033
2057
  let heading = null;
2034
2058
  if (headingData?.trueHeading !== void 0 && headingData.trueHeading >= 0) {
2035
2059
  heading = headingData.trueHeading;
2036
2060
  } else if (headingData?.magneticHeading !== void 0 && headingData.magneticHeading >= 0) {
2037
2061
  heading = headingData.magneticHeading;
2038
- } else if (headingData?.heading !== null && headingData.heading >= 0) {
2062
+ } else if (headingData?.heading !== null && headingData?.heading >= 0) {
2039
2063
  heading = headingData.heading;
2040
2064
  }
2041
2065
  return {
@@ -2505,7 +2529,14 @@ var import_jsx_runtime11 = require("react/jsx-runtime");
2505
2529
  var MockRadiusLayer = ({
2506
2530
  center,
2507
2531
  radiusMeters,
2508
- color = "#39ff88"
2532
+ // Neutral gray, not any one app's accent color — this debug aid is shared
2533
+ // across every mini-app (each with its own, different palette), so it
2534
+ // shouldn't default to a color that reads as "part of the app" (was
2535
+ // #39ff88, a neon green/yellow left over from an earlier shared theme,
2536
+ // reported as looking like an unwanted yellow border around the user's
2537
+ // position marker in up-miniapps-explore, which has since moved to a
2538
+ // pastel palette with no neon accents at all).
2539
+ color = "#e2e8f0"
2509
2540
  }) => {
2510
2541
  const data = (0, import_react10.useMemo)(
2511
2542
  () => buildGeoCircle(center, radiusMeters),
@@ -3680,7 +3711,7 @@ var RoutingService = {
3680
3711
  };
3681
3712
 
3682
3713
  // package.json
3683
- var version = "0.5.7";
3714
+ var version = "0.6.1";
3684
3715
 
3685
3716
  // src/sdk.ts
3686
3717
  function deepFreeze(obj) {
@@ -3695,6 +3726,7 @@ function deepFreeze(obj) {
3695
3726
  return Object.freeze(obj);
3696
3727
  }
3697
3728
  var globalRoutingConfig = null;
3729
+ var lastKnownEnv = null;
3698
3730
  var MiniAppSDK = class {
3699
3731
  /**
3700
3732
  * Map services.
@@ -4017,7 +4049,14 @@ var MiniAppSDK = class {
4017
4049
  });
4018
4050
  constructor() {
4019
4051
  this.bridge = new Bridge();
4052
+ this.bridge.on(
4053
+ "UPDATE_CONTEXT" /* UPDATE_CONTEXT */,
4054
+ (data) => {
4055
+ if (data?.env) lastKnownEnv = data.env;
4056
+ }
4057
+ );
4020
4058
  this.initialize = this.initialize.bind(this);
4059
+ this.getEnv = this.getEnv.bind(this);
4021
4060
  this.on = this.on.bind(this);
4022
4061
  this.off = this.off.bind(this);
4023
4062
  }
@@ -4041,8 +4080,22 @@ var MiniAppSDK = class {
4041
4080
  * The starting point for the Mini-App. Must be called before any other SDK methods.
4042
4081
  * @param options The options to provide to the shell app
4043
4082
  */
4044
- initialize(options) {
4045
- return this.bridge.send("INITIALIZE", options);
4083
+ async initialize(options) {
4084
+ const res = await this.bridge.send(
4085
+ "INITIALIZE",
4086
+ options
4087
+ );
4088
+ if (res?.env) lastKnownEnv = res.env;
4089
+ return res;
4090
+ }
4091
+ /**
4092
+ * Returns the last known `env` (shell-provided config like `backendUrl`),
4093
+ * refreshed automatically by `initialize()` and every `UPDATE_CONTEXT`
4094
+ * push — read this instead of caching your own copy of `env` values.
4095
+ * Returns `null` before the first successful `initialize()`/context push.
4096
+ */
4097
+ getEnv() {
4098
+ return lastKnownEnv;
4046
4099
  }
4047
4100
  /**
4048
4101
  * Registers a callback for a specific event from the native shell.
@@ -5249,6 +5302,207 @@ function handleClearRoute(map, miniAppId) {
5249
5302
  if (map.getSource(sourceId)) map.removeSource(sourceId);
5250
5303
  }
5251
5304
 
5305
+ // src/services/apiService.ts
5306
+ function buildUrl(baseUrl, path) {
5307
+ if (path.startsWith("http://") || path.startsWith("https://")) {
5308
+ return path;
5309
+ }
5310
+ let cleanBase = baseUrl.trim();
5311
+ while (cleanBase.endsWith("/")) {
5312
+ cleanBase = cleanBase.slice(0, -1);
5313
+ }
5314
+ let cleanPath = path.trim();
5315
+ if (!cleanPath.startsWith("/")) {
5316
+ cleanPath = `/${cleanPath}`;
5317
+ }
5318
+ const baseEndsWithV1 = cleanBase.endsWith("/api/v1");
5319
+ const pathStartsWithV1 = cleanPath.startsWith("/api/v1/") || cleanPath === "/api/v1";
5320
+ if (baseEndsWithV1 && pathStartsWithV1) {
5321
+ const strippedPath = cleanPath.slice(7);
5322
+ return `${cleanBase}${strippedPath}`;
5323
+ }
5324
+ return `${cleanBase}${cleanPath}`;
5325
+ }
5326
+ function getEarlyBackendUrl(kind) {
5327
+ if (typeof window === "undefined") return void 0;
5328
+ const early = window.__UP_BACKEND_URLS__;
5329
+ return early?.[kind] ?? void 0;
5330
+ }
5331
+ function createApiService(options) {
5332
+ const getBaseUrl = () => getEarlyBackendUrl("http") || sdk.getEnv()?.backendUrl || options.defaultBaseUrl;
5333
+ const getWsUrl = () => getEarlyBackendUrl("ws") || sdk.getEnv()?.wsBackendUrl || options.defaultWsBackendUrl;
5334
+ return {
5335
+ get baseUrl() {
5336
+ return getBaseUrl();
5337
+ },
5338
+ delete: (path) => sdk.network.request({
5339
+ method: "DELETE",
5340
+ url: buildUrl(getBaseUrl(), path)
5341
+ }),
5342
+ get: (path) => sdk.network.request({
5343
+ headers: { "Content-Type": "application/json" },
5344
+ method: "GET",
5345
+ url: buildUrl(getBaseUrl(), path)
5346
+ }),
5347
+ graphqlQuery: (query, variables = {}) => sdk.network.request({
5348
+ body: { query, variables },
5349
+ headers: { "Content-Type": "application/json" },
5350
+ method: "POST",
5351
+ url: buildUrl(getBaseUrl(), "/graphql")
5352
+ }),
5353
+ patch: (path, body) => sdk.network.request({
5354
+ body,
5355
+ headers: { "Content-Type": "application/json" },
5356
+ method: "PATCH",
5357
+ url: buildUrl(getBaseUrl(), path)
5358
+ }),
5359
+ post: (path, body) => sdk.network.request({
5360
+ body,
5361
+ headers: { "Content-Type": "application/json" },
5362
+ method: "POST",
5363
+ url: buildUrl(getBaseUrl(), path)
5364
+ }),
5365
+ get wsUrl() {
5366
+ return getWsUrl();
5367
+ }
5368
+ };
5369
+ }
5370
+
5371
+ // src/services/wsService.ts
5372
+ function createWsService(options) {
5373
+ const channelField = options.channelField;
5374
+ let ws = null;
5375
+ const subscriptions = /* @__PURE__ */ new Map();
5376
+ const globalListeners = /* @__PURE__ */ new Set();
5377
+ let reconnectTimeout = null;
5378
+ let reconnectDelay = 1e3;
5379
+ let isConnecting = false;
5380
+ const getWsUrl = () => options.baseUrl || options.api.wsUrl || "";
5381
+ const sendAction = (action, channelId) => {
5382
+ if (ws?.readyState === WebSocket.OPEN) {
5383
+ ws.send(JSON.stringify({ action, [channelField]: channelId }));
5384
+ }
5385
+ };
5386
+ const scheduleReconnect = () => {
5387
+ if (reconnectTimeout) clearTimeout(reconnectTimeout);
5388
+ reconnectTimeout = setTimeout(() => {
5389
+ reconnectDelay = Math.min(reconnectDelay * 2, 3e4);
5390
+ connect();
5391
+ }, reconnectDelay);
5392
+ };
5393
+ const connect = async () => {
5394
+ if (ws?.readyState === WebSocket.OPEN || ws?.readyState === WebSocket.CONNECTING || isConnecting) {
5395
+ return;
5396
+ }
5397
+ isConnecting = true;
5398
+ try {
5399
+ const wsUrl = getWsUrl();
5400
+ if (!wsUrl) {
5401
+ console.warn(
5402
+ "[WS] No WebSocket backend URL available, connection delayed."
5403
+ );
5404
+ isConnecting = false;
5405
+ return;
5406
+ }
5407
+ let authParams = {};
5408
+ if (isRunningInsideNativeShell() && options.ticketPath !== null) {
5409
+ const ticketResponse = await options.api.post(
5410
+ options.ticketPath ?? "/ws-ticket"
5411
+ );
5412
+ authParams = { ticket: ticketResponse.data.ticket };
5413
+ } else if (options.devToken) {
5414
+ const rawJwt = options.devToken.startsWith("Bearer ") ? options.devToken.slice(7) : options.devToken;
5415
+ authParams = { token: rawJwt };
5416
+ } else if (options.requireAuth === false) {
5417
+ } else {
5418
+ console.warn("[WS] No auth available, connection delayed.");
5419
+ isConnecting = false;
5420
+ return;
5421
+ }
5422
+ const params = new URLSearchParams({ ...options.query, ...authParams });
5423
+ const query = params.toString();
5424
+ ws = new WebSocket(query ? `${wsUrl}?${query}` : wsUrl);
5425
+ ws.onopen = () => {
5426
+ isConnecting = false;
5427
+ reconnectDelay = 1e3;
5428
+ if (channelField) {
5429
+ for (const channelId of subscriptions.keys()) {
5430
+ sendAction("subscribe", channelId);
5431
+ }
5432
+ }
5433
+ };
5434
+ ws.onmessage = (event) => {
5435
+ try {
5436
+ const payload = JSON.parse(event.data);
5437
+ if (channelField) {
5438
+ const channelId = payload[channelField];
5439
+ if (typeof channelId === "string") {
5440
+ const callbacks = subscriptions.get(channelId);
5441
+ if (callbacks) {
5442
+ for (const cb of callbacks) cb(payload);
5443
+ }
5444
+ }
5445
+ }
5446
+ for (const cb of globalListeners) cb(payload);
5447
+ } catch (err) {
5448
+ console.error("[WS] Failed to parse message:", err);
5449
+ }
5450
+ };
5451
+ ws.onclose = () => {
5452
+ isConnecting = false;
5453
+ scheduleReconnect();
5454
+ };
5455
+ ws.onerror = (err) => {
5456
+ console.error("[WS] error:", err);
5457
+ isConnecting = false;
5458
+ ws?.close();
5459
+ };
5460
+ } catch (err) {
5461
+ console.error("[WS] Failed to establish connection:", err);
5462
+ isConnecting = false;
5463
+ scheduleReconnect();
5464
+ }
5465
+ };
5466
+ const disconnect = () => {
5467
+ if (reconnectTimeout) {
5468
+ clearTimeout(reconnectTimeout);
5469
+ reconnectTimeout = null;
5470
+ }
5471
+ if (ws) {
5472
+ ws.onclose = null;
5473
+ ws.close();
5474
+ ws = null;
5475
+ }
5476
+ };
5477
+ const subscribe = (channelId, callback) => {
5478
+ const isNewChannel = !subscriptions.has(channelId);
5479
+ if (isNewChannel) subscriptions.set(channelId, /* @__PURE__ */ new Set());
5480
+ subscriptions.get(channelId)?.add(callback);
5481
+ if (isNewChannel) sendAction("subscribe", channelId);
5482
+ connect();
5483
+ return () => {
5484
+ const set = subscriptions.get(channelId);
5485
+ if (!set) return;
5486
+ set.delete(callback);
5487
+ if (set.size === 0) {
5488
+ subscriptions.delete(channelId);
5489
+ sendAction("unsubscribe", channelId);
5490
+ }
5491
+ };
5492
+ };
5493
+ const onMessage = (callback) => {
5494
+ globalListeners.add(callback);
5495
+ connect();
5496
+ return () => {
5497
+ globalListeners.delete(callback);
5498
+ };
5499
+ };
5500
+ if (channelField) {
5501
+ return { connect, disconnect, onMessage, subscribe };
5502
+ }
5503
+ return { connect, disconnect, onMessage };
5504
+ }
5505
+
5252
5506
  // src/index.ts
5253
5507
  var src_default = sdk;
5254
5508
  // Annotate the CommonJS export names for ESM import in node:
@@ -5273,6 +5527,8 @@ var src_default = sdk;
5273
5527
  SDKProvider,
5274
5528
  SwipeCarousel,
5275
5529
  UIEvent,
5530
+ createApiService,
5531
+ createWsService,
5276
5532
  handleClearRoute,
5277
5533
  handleDrawRoute,
5278
5534
  handleRouteRequest,