@robotical/webapp-types 3.19.1 → 3.20.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.
@@ -58,6 +58,7 @@ export default class ApplicationManager {
58
58
  isPhoneApp: () => boolean;
59
59
  constructor();
60
60
  getDeviceInfo(): Promise<void>;
61
+ ensureWifiConnectionSupported(): Promise<boolean>;
61
62
  updateRaftNameInAllStates(raftId: string, name: string): Promise<boolean>;
62
63
  private getConnectedRaftName;
63
64
  /**
@@ -68,12 +68,14 @@ import draggableModalState from "../../state-observables/modal/DraggableModalSta
68
68
  import SensorsDashboardModal from "../../components/modals/ SensorsDashboardModal";
69
69
  import LEDLightsOrQRVerificationModal from "../../components/modals/LEDLightsOrQRVerificationModal";
70
70
  import WifiConnectionModal from "../../components/modals/WifiConnectionModal";
71
+ import WifiPhoneAppUpdateRequiredModal from "../../components/modals/WifiPhoneAppUpdateRequiredModal";
71
72
  import { CarouselNavigatorManager } from "../../utils/CarouselNavigatorManager";
72
73
  import i18n from "../../i18n";
73
74
  import InUnpluggedModalContent from "../../components/modals/InUnplugged";
74
75
  import { cogKeepAwakeManager, } from "../../features/cog-keep-awake";
75
76
  import { normalizeWifiLocator } from "../../utils/wifi-connection";
76
- import { ConnectionMethodPreference, getConnectionMethodPreference, } from "../../store/ConnectionMethodPreference";
77
+ import { ConnectionMethodPreference, getConnectionMethodPreference, setConnectionMethodPreference, } from "../../store/ConnectionMethodPreference";
78
+ import { MINIMUM_PHONE_APP_WIFI_VERSION, isPhoneWrapperWifiSupported, } from "../../utils/phone-app-communication/wifi-connection-support";
77
79
  var SHOW_LOGS = true;
78
80
  var TAG = "ApplicationManager";
79
81
  var UNSUPPORTED_WEB_BLUETOOTH_BROWSER_MESSAGE = "This browser can't connect to Marty or Cog over Bluetooth. Please use Chrome or Edge, or use the iPad app.";
@@ -188,6 +190,49 @@ var ApplicationManager = /** @class */ (function () {
188
190
  });
189
191
  });
190
192
  };
193
+ ApplicationManager.prototype.ensureWifiConnectionSupported = function () {
194
+ var _a, _b, _c, _d;
195
+ return __awaiter(this, void 0, void 0, function () {
196
+ var error_1;
197
+ return __generator(this, function (_e) {
198
+ switch (_e.label) {
199
+ case 0:
200
+ if (!isPhoneApp()) {
201
+ return [2 /*return*/, true];
202
+ }
203
+ if (!!this.deviceInfo) return [3 /*break*/, 4];
204
+ _e.label = 1;
205
+ case 1:
206
+ _e.trys.push([1, 3, , 4]);
207
+ return [4 /*yield*/, this.getDeviceInfo()];
208
+ case 2:
209
+ _e.sent();
210
+ return [3 /*break*/, 4];
211
+ case 3:
212
+ error_1 = _e.sent();
213
+ Logger.warn(SHOW_LOGS, TAG, "Failed to determine phone app version for Wi-Fi: ".concat(error_1));
214
+ return [3 /*break*/, 4];
215
+ case 4:
216
+ if (isPhoneWrapperWifiSupported((_a = this.deviceInfo) === null || _a === void 0 ? void 0 : _a.appVersion)) {
217
+ return [2 /*return*/, true];
218
+ }
219
+ // Fail closed on old, missing, or malformed wrapper versions. Resetting
220
+ // the preference also covers a stale Wi-Fi choice left in localStorage.
221
+ setConnectionMethodPreference(ConnectionMethodPreference.BLUETOOTH);
222
+ return [4 /*yield*/, modalState.setModal(createElement(WifiPhoneAppUpdateRequiredModal, {
223
+ systemName: (_b = this.deviceInfo) === null || _b === void 0 ? void 0 : _b.systemName,
224
+ showUpdateAction: isPhoneWrapperWifiSupported((_c = this.deviceInfo) === null || _c === void 0 ? void 0 : _c.latestAppVersion),
225
+ }), i18n.t("connection.wifi_phone_update_title", {
226
+ defaultValue: "Update required for Wi-Fi",
227
+ }), "robotical", true, false)];
228
+ case 5:
229
+ _e.sent();
230
+ Logger.warn(SHOW_LOGS, TAG, "Wi-Fi requires phone app ".concat(MINIMUM_PHONE_APP_WIFI_VERSION, " or later; received ").concat(((_d = this.deviceInfo) === null || _d === void 0 ? void 0 : _d.appVersion) || "unknown"));
231
+ return [2 /*return*/, false];
232
+ }
233
+ });
234
+ });
235
+ };
191
236
  ApplicationManager.prototype.updateRaftNameInAllStates = function (raftId, name) {
192
237
  return __awaiter(this, void 0, void 0, function () {
193
238
  var raft, wasSuccessful;
@@ -224,7 +269,7 @@ var ApplicationManager = /** @class */ (function () {
224
269
  };
225
270
  ApplicationManager.prototype.getConnectedRaftName = function (raft) {
226
271
  return __awaiter(this, void 0, void 0, function () {
227
- var cachedName, error_1;
272
+ var cachedName, error_2;
228
273
  return __generator(this, function (_a) {
229
274
  switch (_a.label) {
230
275
  case 0:
@@ -238,8 +283,8 @@ var ApplicationManager = /** @class */ (function () {
238
283
  return [4 /*yield*/, raft.getRaftName()];
239
284
  case 2: return [2 /*return*/, (_a.sent()) || ""];
240
285
  case 3:
241
- error_1 = _a.sent();
242
- Logger.warn(SHOW_LOGS, TAG, "Failed to read RAFT name for ".concat(raft.id, ": ").concat(error_1));
286
+ error_2 = _a.sent();
287
+ Logger.warn(SHOW_LOGS, TAG, "Failed to read RAFT name for ".concat(raft.id, ": ").concat(error_2));
243
288
  return [2 /*return*/, ""];
244
289
  case 4: return [2 /*return*/];
245
290
  }
@@ -282,7 +327,7 @@ var ApplicationManager = /** @class */ (function () {
282
327
  };
283
328
  ApplicationManager.prototype.confirmQrVerifiedConnection = function (raft) {
284
329
  return __awaiter(this, void 0, void 0, function () {
285
- var didPublishVerifiedEvent, error_2;
330
+ var didPublishVerifiedEvent, error_3;
286
331
  return __generator(this, function (_a) {
287
332
  switch (_a.label) {
288
333
  case 0: return [4 /*yield*/, Promise.resolve()];
@@ -303,8 +348,8 @@ var ApplicationManager = /** @class */ (function () {
303
348
  }
304
349
  return [3 /*break*/, 5];
305
350
  case 4:
306
- error_2 = _a.sent();
307
- Logger.warn(SHOW_LOGS, TAG, "Failed to publish QR verified event for ".concat(raft.id, ": ").concat(error_2));
351
+ error_3 = _a.sent();
352
+ Logger.warn(SHOW_LOGS, TAG, "Failed to publish QR verified event for ".concat(raft.id, ": ").concat(error_3));
308
353
  this.publishLocalVerifiedConnection(raft);
309
354
  return [3 /*break*/, 5];
310
355
  case 5: return [2 /*return*/];
@@ -632,9 +677,9 @@ var ApplicationManager = /** @class */ (function () {
632
677
  };
633
678
  ApplicationManager.prototype._resolveConnectOptions = function (options) {
634
679
  return __awaiter(this, void 0, void 0, function () {
635
- var method, expectedRaftType, bluetoothMethod, locator, modalLocator;
636
- return __generator(this, function (_a) {
637
- switch (_a.label) {
680
+ var method, expectedRaftType, bluetoothMethod, _a, locator, modalLocator;
681
+ return __generator(this, function (_b) {
682
+ switch (_b.label) {
638
683
  case 0:
639
684
  method = options.method;
640
685
  expectedRaftType = options.expectedRaftType;
@@ -647,6 +692,16 @@ var ApplicationManager = /** @class */ (function () {
647
692
  ? RaftConnectionMethod.WEB_SOCKET
648
693
  : bluetoothMethod;
649
694
  }
695
+ _a = method === RaftConnectionMethod.WEB_SOCKET;
696
+ if (!_a) return [3 /*break*/, 2];
697
+ return [4 /*yield*/, this.ensureWifiConnectionSupported()];
698
+ case 1:
699
+ _a = !(_b.sent());
700
+ _b.label = 2;
701
+ case 2:
702
+ if (_a) {
703
+ return [2 /*return*/, null];
704
+ }
650
705
  if (expectedRaftType === RaftTypeE.COG &&
651
706
  method === RaftConnectionMethod.WEB_SOCKET) {
652
707
  this.toaster.error(i18n.t("connection.cog_wifi_unsupported", {
@@ -671,18 +726,18 @@ var ApplicationManager = /** @class */ (function () {
671
726
  return [2 /*return*/, null];
672
727
  }
673
728
  locator = options.locator;
674
- if (!!locator) return [3 /*break*/, 2];
729
+ if (!!locator) return [3 /*break*/, 4];
675
730
  return [4 /*yield*/, modalState.setModal(createElement(WifiConnectionModal, { raftType: expectedRaftType }), i18n.t("connection.wifi_connection_title", {
676
731
  defaultValue: "Connect over Wi-Fi",
677
732
  }), getModalLogoForRaftType(expectedRaftType), true, false)];
678
- case 1:
679
- modalLocator = _a.sent();
733
+ case 3:
734
+ modalLocator = _b.sent();
680
735
  if (!modalLocator) {
681
736
  return [2 /*return*/, null];
682
737
  }
683
738
  locator = modalLocator;
684
- _a.label = 2;
685
- case 2:
739
+ _b.label = 4;
740
+ case 4:
686
741
  if (!locator) {
687
742
  return [2 /*return*/, null];
688
743
  }
@@ -0,0 +1,6 @@
1
+ type WifiPhoneAppUpdateRequiredModalProps = {
2
+ systemName?: string;
3
+ showUpdateAction?: boolean;
4
+ };
5
+ export default function WifiPhoneAppUpdateRequiredModal({ systemName, showUpdateAction, }: WifiPhoneAppUpdateRequiredModalProps): import("react/jsx-runtime").JSX.Element;
6
+ export {};
@@ -0,0 +1,44 @@
1
+ var __assign = (this && this.__assign) || function () {
2
+ __assign = Object.assign || function(t) {
3
+ for (var s, i = 1, n = arguments.length; i < n; i++) {
4
+ s = arguments[i];
5
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
6
+ t[p] = s[p];
7
+ }
8
+ return t;
9
+ };
10
+ return __assign.apply(this, arguments);
11
+ };
12
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
13
+ import { useTranslation } from "react-i18next";
14
+ import modalState from "../../../state-observables/modal/ModalState";
15
+ import { PALE_BLUE } from "../../../styles/colors";
16
+ import openNewTab from "../../../utils/open-new-tab";
17
+ import SimpleButton from "../../disposables/buttons/SimpleButton";
18
+ import styles from "./styles.module.css";
19
+ var IOS_APP_STORE_URL = "https://apps.apple.com/app/robotical/id6739531583";
20
+ var ANDROID_PLAY_STORE_URL = "https://play.google.com/store/apps/details?id=com.marty";
21
+ var getStoreUrl = function (systemName) {
22
+ var platform = "".concat(systemName || "", " ").concat(navigator.userAgent).toLowerCase();
23
+ return platform.includes("android")
24
+ ? ANDROID_PLAY_STORE_URL
25
+ : IOS_APP_STORE_URL;
26
+ };
27
+ export default function WifiPhoneAppUpdateRequiredModal(_a) {
28
+ var systemName = _a.systemName, _b = _a.showUpdateAction, showUpdateAction = _b === void 0 ? false : _b;
29
+ var t = useTranslation().t;
30
+ var keepUsingBluetooth = function () {
31
+ modalState.closeModal(false);
32
+ };
33
+ var updateApp = function () {
34
+ openNewTab(getStoreUrl(systemName));
35
+ modalState.closeModal(false);
36
+ };
37
+ return (_jsxs("div", __assign({ className: styles.container }, { children: [_jsx("p", __assign({ className: styles.message }, { children: t("connection.wifi_phone_update_message", {
38
+ defaultValue: "Wi-Fi connections require Robotical app 2.4.4 or later. You can continue connecting with Bluetooth.",
39
+ }) })), _jsxs("div", __assign({ className: styles.actions }, { children: [_jsx(SimpleButton, { title: t("connection.keep_using_bluetooth", {
40
+ defaultValue: "Keep using Bluetooth",
41
+ }), onClick: keepUsingBluetooth, colour: PALE_BLUE }), showUpdateAction ? (_jsx(SimpleButton, { title: t("connection.update_phone_app", {
42
+ defaultValue: "Update app",
43
+ }), onClick: updateApp })) : null] }))] })));
44
+ }
@@ -2,6 +2,17 @@ import type { UserDeviceInfo } from "../../types/userDeviceInfo";
2
2
  export type FeedbackRating = 1 | 2 | 3 | 4 | 5;
3
3
  export type FeedbackSource = "emoji-modal" | "detailed-feedback" | "help-bug-report";
4
4
  export type FeedbackAppRuntime = "phone-app" | "web-app";
5
+ export type FeedbackScreenshot = {
6
+ id: string;
7
+ fileName: string;
8
+ mimeType: string;
9
+ sizeBytes: number;
10
+ width: number;
11
+ height: number;
12
+ };
13
+ export type FeedbackScreenshotUpload = FeedbackScreenshot & {
14
+ dataUrl: string;
15
+ };
5
16
  export type FeedbackEnvironment = {
6
17
  appRuntime: FeedbackAppRuntime;
7
18
  isPhoneApp: boolean;
@@ -21,6 +32,7 @@ export type SubmitFeedbackInput = {
21
32
  context?: Record<string, unknown> | null;
22
33
  source: FeedbackSource;
23
34
  environment?: FeedbackEnvironment;
35
+ screenshots?: FeedbackScreenshotUpload[];
24
36
  };
25
37
  export type FeedbackRecord = {
26
38
  rating: FeedbackRating | null;
@@ -29,6 +41,7 @@ export type FeedbackRecord = {
29
41
  source: FeedbackSource;
30
42
  context: Record<string, unknown> | null;
31
43
  environment: FeedbackEnvironment;
44
+ screenshots: FeedbackScreenshot[];
32
45
  timestamp: string;
33
46
  text: string;
34
47
  };
@@ -202,6 +202,9 @@ export function buildFeedbackText(record) {
202
202
  if (record.rating) {
203
203
  lines.push("Rating: ".concat(record.rating, "/5"));
204
204
  }
205
+ if (record.screenshots.length > 0) {
206
+ lines.push("Screenshots: ".concat(record.screenshots.length, " (view in Marty Web App Insights)"));
207
+ }
205
208
  lines.push("Email: ".concat(record.email || "Anonymous"));
206
209
  if (record.message) {
207
210
  lines.push("Message: ".concat(record.message));
@@ -209,7 +212,7 @@ export function buildFeedbackText(record) {
209
212
  return lines.join("\n");
210
213
  }
211
214
  export function createFeedbackRecord(input) {
212
- var _a, _b;
215
+ var _a, _b, _c;
213
216
  var normalizedMessage = normalizeOptionalString(input.message);
214
217
  var normalizedEmail = normalizeOptionalString(input.email);
215
218
  var timestamp = new Date().toISOString();
@@ -221,6 +224,14 @@ export function createFeedbackRecord(input) {
221
224
  source: input.source,
222
225
  context: input.context || null,
223
226
  environment: environment,
227
+ screenshots: ((_c = input.screenshots) !== null && _c !== void 0 ? _c : []).map(function (screenshot) { return ({
228
+ id: screenshot.id,
229
+ fileName: screenshot.fileName,
230
+ mimeType: screenshot.mimeType,
231
+ sizeBytes: screenshot.sizeBytes,
232
+ width: screenshot.width,
233
+ height: screenshot.height,
234
+ }); }),
224
235
  timestamp: timestamp,
225
236
  text: "",
226
237
  };
@@ -228,11 +239,12 @@ export function createFeedbackRecord(input) {
228
239
  return record;
229
240
  }
230
241
  export function submitFeedback(input) {
231
- var _a;
242
+ var _a, _b;
232
243
  return __awaiter(this, void 0, void 0, function () {
233
- var requiresRobotConnection, robotConnection, record, firebaseResponse, errorText, slackDelivered, slackResponse, errorText, error_1;
234
- return __generator(this, function (_b) {
235
- switch (_b.label) {
244
+ var requiresRobotConnection, robotConnection, record, feedbackId, firebaseUpdates, _i, _c, screenshot, firebaseResponse, errorText, slackDelivered, slackResponse, errorText, error_1;
245
+ var _d;
246
+ return __generator(this, function (_e) {
247
+ switch (_e.label) {
236
248
  case 0:
237
249
  requiresRobotConnection = isRobotConnectionRequiredForFeedbackSource(input.source);
238
250
  robotConnection = requiresRobotConnection
@@ -243,27 +255,35 @@ export function submitFeedback(input) {
243
255
  }
244
256
  record = createFeedbackRecord(__assign(__assign({}, input), { context: robotConnection
245
257
  ? __assign(__assign({}, ((_a = input.context) !== null && _a !== void 0 ? _a : {})), robotConnection) : input.context }));
246
- return [4 /*yield*/, fetch("".concat(WEB_APP_FEEDBACK_DB_URL).concat(randomHashGenerator(), ".json"), {
258
+ feedbackId = randomHashGenerator();
259
+ firebaseUpdates = (_d = {},
260
+ _d[feedbackId] = record,
261
+ _d);
262
+ for (_i = 0, _c = (_b = input.screenshots) !== null && _b !== void 0 ? _b : []; _i < _c.length; _i++) {
263
+ screenshot = _c[_i];
264
+ firebaseUpdates["~screenshots/".concat(feedbackId, "/").concat(screenshot.id)] = screenshot.dataUrl;
265
+ }
266
+ return [4 /*yield*/, fetch("".concat(WEB_APP_FEEDBACK_DB_URL, ".json?print=silent"), {
247
267
  method: "PATCH",
248
268
  headers: {
249
269
  Application: "application/json",
250
270
  "Content-Type": "application/json",
251
271
  },
252
- body: JSON.stringify(record),
272
+ body: JSON.stringify(firebaseUpdates),
253
273
  })];
254
274
  case 1:
255
- firebaseResponse = _b.sent();
275
+ firebaseResponse = _e.sent();
256
276
  if (!!firebaseResponse.ok) return [3 /*break*/, 3];
257
277
  return [4 /*yield*/, firebaseResponse.text()];
258
278
  case 2:
259
- errorText = _b.sent();
279
+ errorText = _e.sent();
260
280
  Logger.error(SHOW_LOGS, TAG, "Failed to submit feedback to Firebase: ".concat(firebaseResponse.status, " ").concat(errorText));
261
281
  throw new Error("Failed to submit feedback");
262
282
  case 3:
263
283
  slackDelivered = true;
264
- _b.label = 4;
284
+ _e.label = 4;
265
285
  case 4:
266
- _b.trys.push([4, 8, , 9]);
286
+ _e.trys.push([4, 8, , 9]);
267
287
  return [4 /*yield*/, fetch(WEB_APP_FEEDBACK_SLACK_WEBHOOK_URL, {
268
288
  method: "POST",
269
289
  headers: {
@@ -272,15 +292,15 @@ export function submitFeedback(input) {
272
292
  body: JSON.stringify({ text: record.text }),
273
293
  })];
274
294
  case 5:
275
- slackResponse = _b.sent();
295
+ slackResponse = _e.sent();
276
296
  if (!!slackResponse.ok) return [3 /*break*/, 7];
277
297
  return [4 /*yield*/, slackResponse.text()];
278
298
  case 6:
279
- errorText = _b.sent();
299
+ errorText = _e.sent();
280
300
  throw new Error("Failed Slack response: ".concat(slackResponse.status, " ").concat(errorText));
281
301
  case 7: return [3 /*break*/, 9];
282
302
  case 8:
283
- error_1 = _b.sent();
303
+ error_1 = _e.sent();
284
304
  slackDelivered = false;
285
305
  Logger.error(SHOW_LOGS, TAG, "Failed to submit feedback to Slack", error_1);
286
306
  return [3 /*break*/, 9];
@@ -0,0 +1 @@
1
+ export default function openNewTab(url: string): void;
@@ -0,0 +1,5 @@
1
+ export default function openNewTab(url) {
2
+ var res = window.open(url, "_blank");
3
+ res && res.focus();
4
+ }
5
+ ;
@@ -0,0 +1,2 @@
1
+ export declare const MINIMUM_PHONE_APP_WIFI_VERSION = "2.4.4";
2
+ export declare const isPhoneWrapperWifiSupported: (appVersion: string | null | undefined) => boolean;
@@ -0,0 +1,13 @@
1
+ import semverGte from "semver/functions/gte";
2
+ export var MINIMUM_PHONE_APP_WIFI_VERSION = "2.4.4";
3
+ export var isPhoneWrapperWifiSupported = function (appVersion) {
4
+ if (typeof appVersion !== "string" || !appVersion.trim()) {
5
+ return false;
6
+ }
7
+ try {
8
+ return semverGte(appVersion.trim(), MINIMUM_PHONE_APP_WIFI_VERSION);
9
+ }
10
+ catch (_a) {
11
+ return false;
12
+ }
13
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@robotical/webapp-types",
3
- "version": "3.19.1",
3
+ "version": "3.20.0",
4
4
  "description": "Type definitions for the Application Manager",
5
5
  "main": "dist/application-manager.d.ts",
6
6
  "types": "dist/application-manager.d.ts",