@mentra/crust 3.2.1 → 3.3.0-beta.1000
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 +27 -0
- package/android/build.gradle +24 -9
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/java/com/mentra/crust/CrustModule.kt +44 -15
- package/android/src/main/java/com/mentra/crust/http/NativeHttpFailure.kt +31 -0
- package/android/src/main/java/com/mentra/crust/http/NativeHttpRequests.kt +86 -0
- package/android/src/main/java/com/mentra/crust/jsc/JSCPolyfillBridge.kt +69 -54
- package/android/src/main/java/com/mentra/crust/jsc/JSCRuntime.kt +65 -8
- package/android/src/main/java/com/mentra/crust/jsc/JSDeliveryDiagnostics.kt +44 -0
- package/android/src/main/java/com/mentra/crust/navigation/NavigationManager.kt +4 -0
- package/android/src/main/java/com/mentra/crust/preview/PixelCopyPreview.kt +76 -0
- package/android/src/test/java/com/mentra/crust/http/NativeHttpFailureTest.kt +44 -0
- package/android/src/test/java/com/mentra/crust/http/NativeHttpRequestsTest.kt +133 -0
- package/android/src/test/java/com/mentra/crust/jsc/FetchBridgeTest.kt +199 -0
- package/android/src/test/java/com/mentra/crust/jsc/JSDeliveryDiagnosticsTest.kt +54 -0
- package/android/src/test/java/com/mentra/crust/jsc/QuickJsLifecycleTest.kt +87 -0
- package/build/Crust.types.d.ts +25 -11
- package/build/Crust.types.d.ts.map +1 -1
- package/build/Crust.types.js.map +1 -1
- package/build/CrustModule.d.ts +9 -2
- package/build/CrustModule.d.ts.map +1 -1
- package/build/CrustModule.js.map +1 -1
- package/build/CrustModule.web.d.ts +3 -1
- package/build/CrustModule.web.d.ts.map +1 -1
- package/build/CrustModule.web.js +20 -8
- package/build/CrustModule.web.js.map +1 -1
- package/build/index.d.ts +1 -0
- package/build/index.d.ts.map +1 -1
- package/build/index.js +1 -0
- package/build/index.js.map +1 -1
- package/build/nativeHttpRequest.d.ts +13 -0
- package/build/nativeHttpRequest.d.ts.map +1 -0
- package/build/nativeHttpRequest.js +51 -0
- package/build/nativeHttpRequest.js.map +1 -0
- package/build/nativeHttpRequest.test.d.ts +2 -0
- package/build/nativeHttpRequest.test.d.ts.map +1 -0
- package/build/nativeHttpRequest.test.js +88 -0
- package/build/nativeHttpRequest.test.js.map +1 -0
- package/ios/Crust.podspec +1 -1
- package/ios/CrustModule.swift +18 -7
- package/ios/PermissionStatusReceipt.swift +105 -0
- package/ios/Resources/startup.js +154 -68
- package/ios/Source/JSCPolyfillBridge.swift +31 -14
- package/package.json +2 -2
- package/plugin/build/index.d.ts +2 -0
- package/plugin/build/index.js +2 -0
- package/plugin/build/withAndroid.js +39 -0
- package/src/Crust.types.ts +26 -12
- package/src/CrustModule.ts +9 -1
- package/src/CrustModule.web.ts +18 -7
- package/src/index.ts +1 -0
- package/src/nativeHttpRequest.test.ts +94 -0
- package/src/nativeHttpRequest.ts +74 -0
- package/android/src/internal/AndroidManifest.xml +0 -9
- package/android/src/internal/java/com/mentra/crust/receivers/CaptionsTesterIncidentReceiver.kt +0 -46
package/ios/Resources/startup.js
CHANGED
|
@@ -436,6 +436,140 @@
|
|
|
436
436
|
}
|
|
437
437
|
;
|
|
438
438
|
g.crypto = cryptoNs;
|
|
439
|
+
const BASE64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
440
|
+
const BASE64_VALUES = new Uint8Array(128).fill(255);
|
|
441
|
+
for (let i = 0; i < BASE64_ALPHABET.length; i++) BASE64_VALUES[BASE64_ALPHABET.charCodeAt(i)] = i;
|
|
442
|
+
const CHARS_PER_CHUNK = 8192;
|
|
443
|
+
function bytesOf(data) {
|
|
444
|
+
return data instanceof ArrayBuffer ? new Uint8Array(data) : new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
|
445
|
+
}
|
|
446
|
+
function bytesToBase64(bytes) {
|
|
447
|
+
const parts = [];
|
|
448
|
+
const chars = [];
|
|
449
|
+
for (let i = 0; i < bytes.length; i += 3) {
|
|
450
|
+
const b1 = bytes[i];
|
|
451
|
+
const b2 = i + 1 < bytes.length ? bytes[i + 1] : 0;
|
|
452
|
+
const b3 = i + 2 < bytes.length ? bytes[i + 2] : 0;
|
|
453
|
+
chars.push(
|
|
454
|
+
BASE64_ALPHABET.charCodeAt(b1 >> 2),
|
|
455
|
+
BASE64_ALPHABET.charCodeAt((b1 & 3) << 4 | b2 >> 4),
|
|
456
|
+
i + 1 < bytes.length ? BASE64_ALPHABET.charCodeAt((b2 & 15) << 2 | b3 >> 6) : 61,
|
|
457
|
+
i + 2 < bytes.length ? BASE64_ALPHABET.charCodeAt(b3 & 63) : 61
|
|
458
|
+
);
|
|
459
|
+
if (chars.length >= CHARS_PER_CHUNK) {
|
|
460
|
+
parts.push(String.fromCharCode.apply(null, chars));
|
|
461
|
+
chars.length = 0;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
parts.push(String.fromCharCode.apply(null, chars));
|
|
465
|
+
return parts.join("");
|
|
466
|
+
}
|
|
467
|
+
function base64ToBytes(base64) {
|
|
468
|
+
const length = base64.length;
|
|
469
|
+
if (length % 4 !== 0) throw new TypeError("Invalid base64 from the native bridge");
|
|
470
|
+
const padding = length === 0 || base64.charCodeAt(length - 1) !== 61 ? 0 : base64.charCodeAt(length - 2) === 61 ? 2 : 1;
|
|
471
|
+
const out = new Uint8Array(length / 4 * 3 - padding);
|
|
472
|
+
const unpadded = padding === 0 ? length : length - 4;
|
|
473
|
+
let codes = 0;
|
|
474
|
+
let values = 0;
|
|
475
|
+
let o = 0;
|
|
476
|
+
for (let i = 0; i < unpadded; i += 4) {
|
|
477
|
+
const c1 = base64.charCodeAt(i);
|
|
478
|
+
const c2 = base64.charCodeAt(i + 1);
|
|
479
|
+
const c3 = base64.charCodeAt(i + 2);
|
|
480
|
+
const c4 = base64.charCodeAt(i + 3);
|
|
481
|
+
const v1 = BASE64_VALUES[c1 & 127];
|
|
482
|
+
const v2 = BASE64_VALUES[c2 & 127];
|
|
483
|
+
const v3 = BASE64_VALUES[c3 & 127];
|
|
484
|
+
const v4 = BASE64_VALUES[c4 & 127];
|
|
485
|
+
codes |= c1 | c2 | c3 | c4;
|
|
486
|
+
values |= v1 | v2 | v3 | v4;
|
|
487
|
+
out[o++] = v1 << 2 | v2 >> 4;
|
|
488
|
+
out[o++] = (v2 & 15) << 4 | v3 >> 2;
|
|
489
|
+
out[o++] = (v3 & 3) << 6 | v4;
|
|
490
|
+
}
|
|
491
|
+
if (padding > 0) {
|
|
492
|
+
const c1 = base64.charCodeAt(unpadded);
|
|
493
|
+
const c2 = base64.charCodeAt(unpadded + 1);
|
|
494
|
+
const c3 = padding === 1 ? base64.charCodeAt(unpadded + 2) : 65;
|
|
495
|
+
const v1 = BASE64_VALUES[c1 & 127];
|
|
496
|
+
const v2 = BASE64_VALUES[c2 & 127];
|
|
497
|
+
const v3 = BASE64_VALUES[c3 & 127];
|
|
498
|
+
codes |= c1 | c2 | c3;
|
|
499
|
+
values |= v1 | v2 | v3;
|
|
500
|
+
out[o++] = v1 << 2 | v2 >> 4;
|
|
501
|
+
if (padding === 1) out[o++] = (v2 & 15) << 4 | v3 >> 2;
|
|
502
|
+
}
|
|
503
|
+
if (codes > 127 || values > 63) throw new TypeError("Invalid base64 from the native bridge");
|
|
504
|
+
return out;
|
|
505
|
+
}
|
|
506
|
+
function decodeUtf8(bytes, fatal, ignoreBOM) {
|
|
507
|
+
const length = bytes.length;
|
|
508
|
+
const parts = [];
|
|
509
|
+
const units = [];
|
|
510
|
+
let i = !ignoreBOM && length >= 3 && bytes[0] === 239 && bytes[1] === 187 && bytes[2] === 191 ? 3 : 0;
|
|
511
|
+
while (i < length) {
|
|
512
|
+
const asciiStart = i;
|
|
513
|
+
while (i < length && bytes[i] < 128) i++;
|
|
514
|
+
if (i > asciiStart) {
|
|
515
|
+
if (units.length > 0) {
|
|
516
|
+
parts.push(String.fromCharCode.apply(null, units));
|
|
517
|
+
units.length = 0;
|
|
518
|
+
}
|
|
519
|
+
for (let start = asciiStart; start < i; start += CHARS_PER_CHUNK) {
|
|
520
|
+
const chunk = bytes.subarray(start, Math.min(i, start + CHARS_PER_CHUNK));
|
|
521
|
+
parts.push(String.fromCharCode.apply(null, chunk));
|
|
522
|
+
}
|
|
523
|
+
if (i >= length) break;
|
|
524
|
+
}
|
|
525
|
+
const lead = bytes[i++];
|
|
526
|
+
let codePoint;
|
|
527
|
+
let needed;
|
|
528
|
+
let lower = 128;
|
|
529
|
+
let upper = 191;
|
|
530
|
+
if (lead >= 194 && lead <= 223) {
|
|
531
|
+
needed = 1;
|
|
532
|
+
codePoint = lead & 31;
|
|
533
|
+
} else if (lead >= 224 && lead <= 239) {
|
|
534
|
+
if (lead === 224) lower = 160;
|
|
535
|
+
if (lead === 237) upper = 159;
|
|
536
|
+
needed = 2;
|
|
537
|
+
codePoint = lead & 15;
|
|
538
|
+
} else if (lead >= 240 && lead <= 244) {
|
|
539
|
+
if (lead === 240) lower = 144;
|
|
540
|
+
if (lead === 244) upper = 143;
|
|
541
|
+
needed = 3;
|
|
542
|
+
codePoint = lead & 7;
|
|
543
|
+
} else {
|
|
544
|
+
needed = -1;
|
|
545
|
+
codePoint = 0;
|
|
546
|
+
}
|
|
547
|
+
while (needed > 0) {
|
|
548
|
+
const next = i < length ? bytes[i] : -1;
|
|
549
|
+
if (next < lower || next > upper) break;
|
|
550
|
+
codePoint = codePoint << 6 | next & 63;
|
|
551
|
+
lower = 128;
|
|
552
|
+
upper = 191;
|
|
553
|
+
needed--;
|
|
554
|
+
i++;
|
|
555
|
+
}
|
|
556
|
+
if (needed !== 0) {
|
|
557
|
+
if (fatal) throw new TypeError("The encoded data was not valid utf-8");
|
|
558
|
+
units.push(65533);
|
|
559
|
+
} else if (codePoint > 65535) {
|
|
560
|
+
codePoint -= 65536;
|
|
561
|
+
units.push(55296 + (codePoint >> 10), 56320 + (codePoint & 1023));
|
|
562
|
+
} else {
|
|
563
|
+
units.push(codePoint);
|
|
564
|
+
}
|
|
565
|
+
if (units.length >= CHARS_PER_CHUNK) {
|
|
566
|
+
parts.push(String.fromCharCode.apply(null, units));
|
|
567
|
+
units.length = 0;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
if (units.length > 0) parts.push(String.fromCharCode.apply(null, units));
|
|
571
|
+
return parts.join("");
|
|
572
|
+
}
|
|
439
573
|
if (typeof g.TextEncoder !== "function") {
|
|
440
574
|
class TextEncoderPolyfill {
|
|
441
575
|
constructor() {
|
|
@@ -476,36 +610,14 @@
|
|
|
476
610
|
}
|
|
477
611
|
if (typeof g.TextDecoder !== "function") {
|
|
478
612
|
class TextDecoderPolyfill {
|
|
479
|
-
constructor(encoding = "utf-8") {
|
|
613
|
+
constructor(encoding = "utf-8", options) {
|
|
480
614
|
this.encoding = encoding;
|
|
615
|
+
this.fatal = options?.fatal === true;
|
|
616
|
+
this.ignoreBOM = options?.ignoreBOM === true;
|
|
481
617
|
}
|
|
482
618
|
decode(buffer) {
|
|
483
619
|
if (!buffer) return "";
|
|
484
|
-
|
|
485
|
-
let out = "";
|
|
486
|
-
let i = 0;
|
|
487
|
-
while (i < bytes.length) {
|
|
488
|
-
const b1 = bytes[i++];
|
|
489
|
-
let code;
|
|
490
|
-
if (b1 < 128) {
|
|
491
|
-
code = b1;
|
|
492
|
-
} else if (b1 < 192) {
|
|
493
|
-
code = 65533;
|
|
494
|
-
} else if (b1 < 224) {
|
|
495
|
-
code = (b1 & 31) << 6 | bytes[i++] & 63;
|
|
496
|
-
} else if (b1 < 240) {
|
|
497
|
-
code = (b1 & 15) << 12 | (bytes[i++] & 63) << 6 | bytes[i++] & 63;
|
|
498
|
-
} else {
|
|
499
|
-
code = (b1 & 7) << 18 | (bytes[i++] & 63) << 12 | (bytes[i++] & 63) << 6 | bytes[i++] & 63;
|
|
500
|
-
}
|
|
501
|
-
if (code > 65535) {
|
|
502
|
-
code -= 65536;
|
|
503
|
-
out += String.fromCharCode(55296 + (code >> 10), 56320 + (code & 1023));
|
|
504
|
-
} else {
|
|
505
|
-
out += String.fromCharCode(code);
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
return out;
|
|
620
|
+
return decodeUtf8(bytesOf(buffer), this.fatal, this.ignoreBOM);
|
|
509
621
|
}
|
|
510
622
|
}
|
|
511
623
|
;
|
|
@@ -552,9 +664,12 @@
|
|
|
552
664
|
const url = typeof input === "string" ? input : input.toString();
|
|
553
665
|
const method = (init?.method ?? "GET").toUpperCase();
|
|
554
666
|
let bodyString = null;
|
|
667
|
+
let bodyBase64;
|
|
555
668
|
const reqBody = init?.body;
|
|
556
669
|
if (typeof reqBody === "string") {
|
|
557
670
|
bodyString = reqBody;
|
|
671
|
+
} else if (reqBody instanceof ArrayBuffer || ArrayBuffer.isView(reqBody)) {
|
|
672
|
+
bodyBase64 = bytesToBase64(bytesOf(reqBody));
|
|
558
673
|
} else if (reqBody && typeof reqBody.toString === "function") {
|
|
559
674
|
bodyString = reqBody.toString();
|
|
560
675
|
}
|
|
@@ -578,9 +693,10 @@
|
|
|
578
693
|
}
|
|
579
694
|
const sendRequest = g.__mentraSendRequest;
|
|
580
695
|
const result = await sendRequest("fetch", "request", [
|
|
581
|
-
{ url, method, headers, body: bodyString }
|
|
696
|
+
bodyBase64 === void 0 ? { url, method, headers, body: bodyString } : { url, method, headers, bodyBase64 }
|
|
582
697
|
]);
|
|
583
|
-
const
|
|
698
|
+
const responseText = typeof result.body === "string" ? result.body : null;
|
|
699
|
+
const responseBase64 = typeof result.bodyBase64 === "string" ? result.bodyBase64 : "";
|
|
584
700
|
const responseHeaders = new Map(Object.entries(result.headers ?? {}));
|
|
585
701
|
class ResponseLike {
|
|
586
702
|
constructor() {
|
|
@@ -597,11 +713,13 @@
|
|
|
597
713
|
};
|
|
598
714
|
}
|
|
599
715
|
async text() {
|
|
600
|
-
return
|
|
716
|
+
if (responseText === null) return decodeUtf8(base64ToBytes(responseBase64), false, false);
|
|
717
|
+
return responseText.charCodeAt(0) === 65279 ? responseText.slice(1) : responseText;
|
|
601
718
|
}
|
|
602
719
|
async json() {
|
|
720
|
+
const text = await this.text();
|
|
603
721
|
try {
|
|
604
|
-
return JSON.parse(
|
|
722
|
+
return JSON.parse(text);
|
|
605
723
|
} catch (e) {
|
|
606
724
|
const err = new Error(
|
|
607
725
|
`Failed to parse JSON response from ${url}: ${e instanceof Error ? e.message : String(e)}`
|
|
@@ -611,52 +729,21 @@
|
|
|
611
729
|
}
|
|
612
730
|
}
|
|
613
731
|
async arrayBuffer() {
|
|
732
|
+
if (responseText === null) return base64ToBytes(responseBase64).buffer;
|
|
614
733
|
const enc = new g.TextEncoder();
|
|
615
|
-
return enc.encode(
|
|
734
|
+
return enc.encode(responseText).buffer;
|
|
616
735
|
}
|
|
617
736
|
}
|
|
618
737
|
return new ResponseLike();
|
|
619
738
|
};
|
|
620
739
|
}
|
|
621
740
|
if (typeof g.WebSocket !== "function") {
|
|
622
|
-
let
|
|
623
|
-
const b64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
624
|
-
let out = "";
|
|
625
|
-
let i = 0;
|
|
626
|
-
while (i < bytes.length) {
|
|
627
|
-
const c1 = bytes[i++];
|
|
628
|
-
const c2 = i < bytes.length ? bytes[i++] : NaN;
|
|
629
|
-
const c3 = i < bytes.length ? bytes[i++] : NaN;
|
|
630
|
-
const e1 = c1 >> 2;
|
|
631
|
-
const e2 = (c1 & 3) << 4 | (Number.isNaN(c2) ? 0 : c2 >> 4);
|
|
632
|
-
const e3 = Number.isNaN(c2) ? 64 : (c2 & 15) << 2 | (Number.isNaN(c3) ? 0 : c3 >> 6);
|
|
633
|
-
const e4 = Number.isNaN(c3) ? 64 : c3 & 63;
|
|
634
|
-
out += b64[e1] + b64[e2] + (e3 === 64 ? "=" : b64[e3]) + (e4 === 64 ? "=" : b64[e4]);
|
|
635
|
-
}
|
|
636
|
-
return out;
|
|
637
|
-
}, base64ToBytes2 = function(s) {
|
|
638
|
-
const b64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
639
|
-
const clean = s.replace(/=+$/, "");
|
|
640
|
-
const out = [];
|
|
641
|
-
let buf = 0;
|
|
642
|
-
let bits = 0;
|
|
643
|
-
for (let i = 0; i < clean.length; i++) {
|
|
644
|
-
const idx = b64.indexOf(clean[i]);
|
|
645
|
-
if (idx < 0) continue;
|
|
646
|
-
buf = buf << 6 | idx;
|
|
647
|
-
bits += 6;
|
|
648
|
-
if (bits >= 8) {
|
|
649
|
-
bits -= 8;
|
|
650
|
-
out.push(buf >> bits & 255);
|
|
651
|
-
}
|
|
652
|
-
}
|
|
653
|
-
return new Uint8Array(out);
|
|
654
|
-
}, queueMicrotaskSafe2 = function(fn) {
|
|
741
|
+
let queueMicrotaskSafe2 = function(fn) {
|
|
655
742
|
const q = g.queueMicrotask;
|
|
656
743
|
if (typeof q === "function") q(fn);
|
|
657
744
|
else g.Promise.resolve().then(fn);
|
|
658
745
|
};
|
|
659
|
-
var
|
|
746
|
+
var queueMicrotaskSafe = queueMicrotaskSafe2;
|
|
660
747
|
const sockets = /* @__PURE__ */ new Map();
|
|
661
748
|
g.__mentraDeliverWebSocketEvent = (sid, type, payload) => {
|
|
662
749
|
const sock = sockets.get(sid);
|
|
@@ -718,8 +805,7 @@
|
|
|
718
805
|
payload = data;
|
|
719
806
|
} else {
|
|
720
807
|
kind = "binary";
|
|
721
|
-
|
|
722
|
-
payload = bytesToBase642(bytes);
|
|
808
|
+
payload = bytesToBase64(bytesOf(data));
|
|
723
809
|
}
|
|
724
810
|
try {
|
|
725
811
|
__dispatch("ws", "send", JSON.stringify([{ sid: this.sid, kind, payload }]));
|
|
@@ -752,7 +838,7 @@
|
|
|
752
838
|
}
|
|
753
839
|
const synth = { type, target: this, ...ev };
|
|
754
840
|
if (type === "message" && ev.kind === "binary" && typeof ev.data === "string") {
|
|
755
|
-
synth.data =
|
|
841
|
+
synth.data = base64ToBytes(ev.data).buffer;
|
|
756
842
|
} else if (type === "message" && ev.kind === "text") {
|
|
757
843
|
synth.data = ev.data;
|
|
758
844
|
}
|
|
@@ -100,14 +100,25 @@ public enum JSCPolyfillBridge {
|
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
var urlRequest = URLRequest(url: url)
|
|
103
|
-
|
|
103
|
+
let method = (req["method"] as? String) ?? "GET"
|
|
104
|
+
urlRequest.httpMethod = method
|
|
104
105
|
if let headers = req["headers"] as? [String: Any] {
|
|
105
106
|
for (k, v) in headers {
|
|
106
107
|
urlRequest.setValue(String(describing: v), forHTTPHeaderField: k)
|
|
107
108
|
}
|
|
108
109
|
}
|
|
109
|
-
|
|
110
|
-
|
|
110
|
+
// ArrayBuffer and typed-array bodies arrive as base64 of their bytes.
|
|
111
|
+
// As on Android, an empty body is still sent, and POST, PUT and PATCH
|
|
112
|
+
// always carry one, so the server sees Content-Length: 0.
|
|
113
|
+
if let encoded = req["bodyBase64"] as? String {
|
|
114
|
+
guard let data = Data(base64Encoded: encoded) else {
|
|
115
|
+
return .error(code: "INVALID_ARGS", message: "fetch.request: bodyBase64 is not base64")
|
|
116
|
+
}
|
|
117
|
+
urlRequest.httpBody = data
|
|
118
|
+
} else if let body = req["body"] as? String {
|
|
119
|
+
urlRequest.httpBody = Data(body.utf8)
|
|
120
|
+
} else if ["POST", "PUT", "PATCH"].contains(method.uppercased()) {
|
|
121
|
+
urlRequest.httpBody = Data()
|
|
111
122
|
}
|
|
112
123
|
|
|
113
124
|
// Reasonable defaults — caller can override later by passing
|
|
@@ -141,21 +152,27 @@ public enum JSCPolyfillBridge {
|
|
|
141
152
|
}
|
|
142
153
|
return out
|
|
143
154
|
}()
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
155
|
+
var result: [String: Any] = [
|
|
156
|
+
"status": status,
|
|
157
|
+
"statusText": HTTPURLResponse.localizedString(forStatusCode: status),
|
|
158
|
+
"headers": headersDict,
|
|
159
|
+
"ok": (200..<300).contains(status),
|
|
160
|
+
]
|
|
161
|
+
// The string `body` only when the bytes are exactly its UTF-8
|
|
162
|
+
// encoding; otherwise every byte as base64. The decode fails
|
|
163
|
+
// on invalid UTF-8, and the round trip also catches a dropped
|
|
164
|
+
// byte order mark.
|
|
165
|
+
let body = data ?? Data()
|
|
166
|
+
if let text = String(data: body, encoding: .utf8), Data(text.utf8) == body {
|
|
167
|
+
result["body"] = text
|
|
168
|
+
} else {
|
|
169
|
+
result["bodyBase64"] = body.base64EncodedString()
|
|
170
|
+
}
|
|
148
171
|
let payload: [String: Any] = [
|
|
149
172
|
"kind": "response",
|
|
150
173
|
"reqId": reqId,
|
|
151
174
|
"ok": true,
|
|
152
|
-
"result":
|
|
153
|
-
"status": status,
|
|
154
|
-
"statusText": HTTPURLResponse.localizedString(forStatusCode: status),
|
|
155
|
-
"headers": headersDict,
|
|
156
|
-
"body": bodyStr,
|
|
157
|
-
"ok": (200..<300).contains(status),
|
|
158
|
-
] as [String: Any],
|
|
175
|
+
"result": result,
|
|
159
176
|
]
|
|
160
177
|
JSCRuntime.shared.dispatchToJs(packageName: packageName, envelope: payload)
|
|
161
178
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mentra/crust",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.3.0-beta.1000",
|
|
4
4
|
"description": "Mentra Native Module",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"types": "build/index.d.ts",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"license": "Apache-2.0",
|
|
36
36
|
"homepage": "https://github.com/Mentra-Community/MentraOS/tree/dev/mobile/modules/crust#readme",
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@mentra/jspolyfill": "3.
|
|
38
|
+
"@mentra/jspolyfill": "3.3.0-beta.1000"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"@expo/config-plugins": ">=8.0.0",
|
package/plugin/build/index.d.ts
CHANGED
|
@@ -14,6 +14,8 @@ import { type ConfigPlugin } from "expo/config-plugins";
|
|
|
14
14
|
* both fails the release build with duplicate classes)
|
|
15
15
|
* - core-library desugaring (crust's AAR metadata requires it of embedding
|
|
16
16
|
* apps — the Nav SDK uses newer core libs)
|
|
17
|
+
* - Kotlin 2.3.0 or newer, with the root Kotlin Gradle plugin pinned to it
|
|
18
|
+
* (crust's QuickJS library, quickjs-kt 1.0.15, is compiled with Kotlin 2.4)
|
|
17
19
|
* - a generated MainApplication process guard so the notification-listener
|
|
18
20
|
* process never initializes React Native
|
|
19
21
|
* - iOS Mapbox Swift Package products and the Crust build-order dependency
|
package/plugin/build/index.js
CHANGED
|
@@ -21,6 +21,8 @@ const withMapboxNavIos_1 = __importDefault(require("./withMapboxNavIos"));
|
|
|
21
21
|
* both fails the release build with duplicate classes)
|
|
22
22
|
* - core-library desugaring (crust's AAR metadata requires it of embedding
|
|
23
23
|
* apps — the Nav SDK uses newer core libs)
|
|
24
|
+
* - Kotlin 2.3.0 or newer, with the root Kotlin Gradle plugin pinned to it
|
|
25
|
+
* (crust's QuickJS library, quickjs-kt 1.0.15, is compiled with Kotlin 2.4)
|
|
24
26
|
* - a generated MainApplication process guard so the notification-listener
|
|
25
27
|
* process never initializes React Native
|
|
26
28
|
* - iOS Mapbox Swift Package products and the Crust build-order dependency
|
|
@@ -20,12 +20,50 @@ const MAPBOX_REPO = [
|
|
|
20
20
|
" }",
|
|
21
21
|
" }",
|
|
22
22
|
].join("\n");
|
|
23
|
+
// quickjs-kt 1.0.15 is compiled with Kotlin 2.4, and a Kotlin compiler reads
|
|
24
|
+
// metadata one version ahead. 2.3.0 is the newest 2.3 release Expo SDK 55's
|
|
25
|
+
// root plugin maps to a KSP version.
|
|
26
|
+
const MIN_KOTLIN_VERSION = "2.3.0";
|
|
27
|
+
// The template's unversioned classpath entry resolves to the Kotlin Gradle
|
|
28
|
+
// plugin React Native's Gradle plugin depends on (2.1.20 on RN 0.83), whatever
|
|
29
|
+
// `android.kotlinVersion` says; Expo fixes that only in SDK 58
|
|
30
|
+
// (expo/expo#49668). SDK 56 also never copies the property into Expo's catalog,
|
|
31
|
+
// so pin both the compiler and the root `kotlinVersion` to the property itself.
|
|
32
|
+
const UNVERSIONED_KOTLIN_PLUGIN = "classpath('org.jetbrains.kotlin:kotlin-gradle-plugin')";
|
|
33
|
+
const PINNED_KOTLIN_PLUGIN = `classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:\${findProperty('android.kotlinVersion')}")`;
|
|
34
|
+
const KOTLIN_VERSION_MARKER = "kotlinVersion from android.kotlinVersion (injected by @mentra/crust)";
|
|
35
|
+
const EXPO_ROOT_PROJECT = 'apply plugin: "expo-root-project"';
|
|
36
|
+
function isOlderVersion(version, minimum) {
|
|
37
|
+
const actual = version.split(".").map(Number);
|
|
38
|
+
const required = minimum.split(".").map(Number);
|
|
39
|
+
for (let i = 0; i < required.length; i++) {
|
|
40
|
+
if ((actual[i] ?? 0) !== required[i])
|
|
41
|
+
return (actual[i] ?? 0) < required[i];
|
|
42
|
+
}
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
function withCrustKotlinVersion(config) {
|
|
46
|
+
return (0, config_plugins_1.withGradleProperties)(config, (cfg) => {
|
|
47
|
+
const property = cfg.modResults.find((item) => item.type === "property" && item.key === "android.kotlinVersion");
|
|
48
|
+
if (property?.type !== "property") {
|
|
49
|
+
cfg.modResults.push({ type: "property", key: "android.kotlinVersion", value: MIN_KOTLIN_VERSION });
|
|
50
|
+
}
|
|
51
|
+
else if (isOlderVersion(property.value, MIN_KOTLIN_VERSION)) {
|
|
52
|
+
property.value = MIN_KOTLIN_VERSION;
|
|
53
|
+
}
|
|
54
|
+
return cfg;
|
|
55
|
+
});
|
|
56
|
+
}
|
|
23
57
|
const PROTOBUF_EXCLUDE = "exclude group: 'com.google.protobuf', module: 'protobuf-javalite'";
|
|
24
58
|
const NOTIFICATION_PROCESS_GUARD = "crust: skip React Native in notification process";
|
|
25
59
|
const NOTIFICATION_CONFIG_GUARD = "crust: skip Expo lifecycle in notification process";
|
|
26
60
|
function withCrustProjectGradle(config) {
|
|
27
61
|
return (0, config_plugins_1.withProjectBuildGradle)(config, (cfg) => {
|
|
28
62
|
let gradle = cfg.modResults.contents;
|
|
63
|
+
gradle = gradle.replace(UNVERSIONED_KOTLIN_PLUGIN, PINNED_KOTLIN_PLUGIN);
|
|
64
|
+
if (!gradle.includes(KOTLIN_VERSION_MARKER)) {
|
|
65
|
+
gradle = gradle.replace(EXPO_ROOT_PROJECT, `// ${KOTLIN_VERSION_MARKER}\next.kotlinVersion = findProperty('android.kotlinVersion')\n${EXPO_ROOT_PROJECT}`);
|
|
66
|
+
}
|
|
29
67
|
// Mapbox Downloads repo, inserted at the top of allprojects.repositories.
|
|
30
68
|
if (!gradle.includes(MAPBOX_REPO_MARKER)) {
|
|
31
69
|
const reposMatch = gradle.match(/allprojects\s*\{[\s\S]*?repositories\s*\{/);
|
|
@@ -129,6 +167,7 @@ function withNotificationProcessGuard(config) {
|
|
|
129
167
|
});
|
|
130
168
|
}
|
|
131
169
|
const withCrustAndroidBuildContract = (config) => {
|
|
170
|
+
config = withCrustKotlinVersion(config);
|
|
132
171
|
config = withCrustProjectGradle(config);
|
|
133
172
|
config = withCrustAppGradle(config);
|
|
134
173
|
return withNotificationProcessGuard(config);
|
package/src/Crust.types.ts
CHANGED
|
@@ -16,7 +16,6 @@ export type CrustModuleEvents = {
|
|
|
16
16
|
onHeading: (params: HeadingPayload) => void
|
|
17
17
|
phone_notification: (event: PhoneNotificationEvent) => void
|
|
18
18
|
phone_notification_dismissed: (event: PhoneNotificationDismissedEvent) => void
|
|
19
|
-
captions_tester_incident: (event: CaptionsTesterIncidentEvent) => void
|
|
20
19
|
}
|
|
21
20
|
|
|
22
21
|
export type NavOffRoutePayload = {
|
|
@@ -114,6 +113,8 @@ export type NavManeuverPayload = {
|
|
|
114
113
|
|
|
115
114
|
export type NavErrorPayload = {
|
|
116
115
|
message: string
|
|
116
|
+
/** True only when native navigation has ended; absent for recoverable reroute errors. */
|
|
117
|
+
terminal?: boolean
|
|
117
118
|
}
|
|
118
119
|
|
|
119
120
|
export type InstalledApp = {
|
|
@@ -144,19 +145,32 @@ export type PhoneNotificationDismissedEvent = {
|
|
|
144
145
|
notificationId: string
|
|
145
146
|
}
|
|
146
147
|
|
|
147
|
-
export type CaptionsTesterIncidentEvent = {
|
|
148
|
-
action?: string
|
|
149
|
-
timestamp?: number
|
|
150
|
-
failure_code?: string
|
|
151
|
-
failure_message?: string
|
|
152
|
-
test_run_id?: string
|
|
153
|
-
scenario_name?: string
|
|
154
|
-
source?: string
|
|
155
|
-
[key: string]: unknown
|
|
156
|
-
}
|
|
157
|
-
|
|
158
148
|
export type CrustViewProps = {
|
|
159
149
|
url: string
|
|
160
150
|
onLoad: (event: {nativeEvent: OnLoadEventPayload}) => void
|
|
161
151
|
style?: StyleProp<ViewStyle>
|
|
162
152
|
}
|
|
153
|
+
|
|
154
|
+
/** Passive OS observations for the currently executing Mentra App. */
|
|
155
|
+
export type AppPermissionStatus = {
|
|
156
|
+
rawStatus: string | null
|
|
157
|
+
status: "granted" | "denied" | "blocked" | "unavailable" | "unknown"
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export type AppPermissionSnapshot = {
|
|
161
|
+
schemaVersion: 1
|
|
162
|
+
observedAt: string
|
|
163
|
+
identity: {
|
|
164
|
+
pid: number
|
|
165
|
+
uid: number
|
|
166
|
+
bundleId: string
|
|
167
|
+
bundlePath: string
|
|
168
|
+
executablePath: string
|
|
169
|
+
version: string
|
|
170
|
+
build: string
|
|
171
|
+
}
|
|
172
|
+
locationServicesEnabled: boolean
|
|
173
|
+
permissions: Record<"location" | "camera" | "microphone" | "bluetooth" | "localNetwork", AppPermissionStatus>
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export type AppPermissionReceipt = AppPermissionSnapshot & {requestId: string}
|
package/src/CrustModule.ts
CHANGED
|
@@ -1,19 +1,27 @@
|
|
|
1
1
|
import {NativeModule, requireNativeModule} from "expo"
|
|
2
2
|
|
|
3
|
-
import {CrustModuleEvents, InstalledApp} from "./Crust.types"
|
|
3
|
+
import {AppPermissionReceipt, AppPermissionSnapshot, CrustModuleEvents, InstalledApp} from "./Crust.types"
|
|
4
4
|
|
|
5
5
|
declare class CrustModule extends NativeModule<CrustModuleEvents> {
|
|
6
6
|
/** True for the native iOS app running on an Apple Silicon Mac. */
|
|
7
7
|
readonly isIOSAppOnMac?: boolean
|
|
8
|
+
/** iOS: passive actual-app authorization status; never requests consent. */
|
|
9
|
+
getPermissionStatus(requestId: null): Promise<AppPermissionSnapshot>
|
|
10
|
+
/** iOS: a nonce publishes fresh status at the fixed managed-cache path. */
|
|
11
|
+
getPermissionStatus(requestId: string): Promise<AppPermissionReceipt>
|
|
8
12
|
PI: number
|
|
9
13
|
hello(): string
|
|
10
14
|
setValueAsync(value: string): Promise<void>
|
|
15
|
+
/** Android: copy the displayed view's window pixels to a temporary JPEG URI. */
|
|
16
|
+
captureMiniappPreview(viewTag: number): Promise<string>
|
|
11
17
|
nativeHttpRequest(
|
|
18
|
+
requestId: string,
|
|
12
19
|
method: string,
|
|
13
20
|
url: string,
|
|
14
21
|
headers: Record<string, string>,
|
|
15
22
|
body?: string | null,
|
|
16
23
|
): Promise<{status: number; statusText: string; headers: Record<string, string>; body: string}>
|
|
24
|
+
cancelNativeHttpRequest(requestId: string): Promise<void>
|
|
17
25
|
showAVRoutePicker(tintColor?: string | null): void
|
|
18
26
|
|
|
19
27
|
/**
|
package/src/CrustModule.web.ts
CHANGED
|
@@ -4,18 +4,29 @@ import {CrustModuleEvents} from "./Crust.types"
|
|
|
4
4
|
|
|
5
5
|
class CrustModule extends NativeModule<CrustModuleEvents> {
|
|
6
6
|
PI = Math.PI
|
|
7
|
+
private readonly httpRequests = new Map<string, AbortController>()
|
|
7
8
|
async setValueAsync(value: string): Promise<void> {
|
|
8
9
|
this.emit("onChange", {value})
|
|
9
10
|
}
|
|
10
|
-
async nativeHttpRequest(method: string, url: string, headers: Record<string, string>, body?: string | null) {
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
11
|
+
async nativeHttpRequest(requestId: string, method: string, url: string, headers: Record<string, string>, body?: string | null) {
|
|
12
|
+
const controller = new AbortController()
|
|
13
|
+
this.httpRequests.set(requestId, controller)
|
|
14
|
+
try {
|
|
15
|
+
const response = await fetch(url, {method, headers, body, signal: controller.signal})
|
|
16
|
+
return {
|
|
17
|
+
status: response.status,
|
|
18
|
+
statusText: response.statusText,
|
|
19
|
+
headers: Object.fromEntries(response.headers.entries()),
|
|
20
|
+
body: await response.text(),
|
|
21
|
+
}
|
|
22
|
+
} finally {
|
|
23
|
+
this.httpRequests.delete(requestId)
|
|
17
24
|
}
|
|
18
25
|
}
|
|
26
|
+
async cancelNativeHttpRequest(requestId: string): Promise<void> {
|
|
27
|
+
this.httpRequests.get(requestId)?.abort()
|
|
28
|
+
this.httpRequests.delete(requestId)
|
|
29
|
+
}
|
|
19
30
|
hello() {
|
|
20
31
|
return "Hello world! 👋"
|
|
21
32
|
}
|
package/src/index.ts
CHANGED