@trillboards/edge-sdk 0.2.137 → 0.2.140
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/deploy/macos/README.md +21 -1
- package/deploy/macos/TrillboardsSetupApp.m +323 -15
- package/deploy/macos/build-macos-pkg.sh +7 -2
- package/deploy/macos/pair.command +3 -1
- package/deploy/setup-guidance.json +112 -0
- package/deploy/windows/build-windows.ps1 +1 -0
- package/deploy/windows/trillboards-setup.ps1 +101 -2
- package/dist/EdgeAgent.js +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +68 -14
- package/dist/cli.js.map +1 -1
- package/dist/diagnose.d.ts +1 -1
- package/dist/diagnose.js +1 -1
- package/dist/installerBrowserSession.d.ts +21 -0
- package/dist/installerBrowserSession.d.ts.map +1 -0
- package/dist/installerBrowserSession.js +128 -0
- package/dist/installerBrowserSession.js.map +1 -0
- package/dist/installerCameraHandoff.d.ts +14 -0
- package/dist/installerCameraHandoff.d.ts.map +1 -0
- package/dist/installerCameraHandoff.js +28 -0
- package/dist/installerCameraHandoff.js.map +1 -0
- package/dist/installerOnboardingMode.d.ts +2 -0
- package/dist/installerOnboardingMode.d.ts.map +1 -1
- package/dist/installerOnboardingMode.js +13 -3
- package/dist/installerOnboardingMode.js.map +1 -1
- package/dist/setupMode.d.ts +2 -0
- package/dist/setupMode.d.ts.map +1 -1
- package/dist/setupMode.js +8 -0
- package/dist/setupMode.js.map +1 -1
- package/package.json +1 -1
package/deploy/macos/README.md
CHANGED
|
@@ -49,7 +49,12 @@ delivered **only by pairing** — it is never baked into the `.pkg`.
|
|
|
49
49
|
**Start setup**. Follow **Welcome → Account → Account setup → Plan & Payment**.
|
|
50
50
|
Sign in with Google or email, choose the account if you manage more than one,
|
|
51
51
|
accept the current agreements, then choose a plan
|
|
52
|
-
and finish secure Stripe checkout.
|
|
52
|
+
and finish secure Stripe checkout. The first browser step opens a **Sense Anything
|
|
53
|
+
setup** tab in the system default browser. Keep it open: Google and checkout
|
|
54
|
+
links originate from this tab so they stay in that browser/profile. **Reopen
|
|
55
|
+
setup tab** is an explicit recovery through the current default browser; it
|
|
56
|
+
revokes the old tab's connection. Agreement review and the authenticated
|
|
57
|
+
recorder/camera wizard open in owned Connector windows on the Mac. Cancelling checkout preserves account setup;
|
|
53
58
|
use **Back to plans** in the Connector to retry. Existing subscribed accounts
|
|
54
59
|
resume at recorder setup. The Connector checks subscription access automatically
|
|
55
60
|
before opening the signed-in Sense Anything camera wizard,
|
|
@@ -180,3 +185,18 @@ runs the local model:
|
|
|
180
185
|
The pairing endpoint (`POST /v2/earner/cameras/pair`), the config shape, and the
|
|
181
186
|
local-first sensing runtime are shared across all platforms — only the
|
|
182
187
|
service-supervisor wrapper differs.
|
|
188
|
+
|
|
189
|
+
### Browser continuity implementation
|
|
190
|
+
|
|
191
|
+
`installerBrowserSession.ts` serves one loopback-only setup tab. Its capability
|
|
192
|
+
is carried in the initial fragment and tab session storage, then authenticated
|
|
193
|
+
in request headers. Host/origin checks reject rebinding and cross-origin access;
|
|
194
|
+
only one tab owner receives pending actions. Later stages never use the OS opener
|
|
195
|
+
unless the user explicitly requests recovery. Links use a user gesture and
|
|
196
|
+
`noopener noreferrer`; Google and Stripe stay in the real external browser.
|
|
197
|
+
|
|
198
|
+
The Mac shell receives the one-use camera grant over inherited pipe descriptor
|
|
199
|
+
198, passed explicitly through `pair.command`. It is never placed in logs or the
|
|
200
|
+
progress file. Camera and legal views use separate nonpersistent WebKit stores
|
|
201
|
+
and restricted navigation hosts. The native release build smoke-tests pipe
|
|
202
|
+
delivery, cancellation, and temporary-directory handling.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#import <Cocoa/Cocoa.h>
|
|
2
|
+
#import <WebKit/WebKit.h>
|
|
2
3
|
#import <errno.h>
|
|
3
4
|
#import <fcntl.h>
|
|
4
5
|
#import <signal.h>
|
|
@@ -37,8 +38,22 @@ static BOOL acquireSetupInstanceLock(void) {
|
|
|
37
38
|
return NO;
|
|
38
39
|
}
|
|
39
40
|
|
|
40
|
-
@interface TrillboardsSetupDelegate : NSObject <NSApplicationDelegate>
|
|
41
|
+
@interface TrillboardsSetupDelegate : NSObject <NSApplicationDelegate, NSWindowDelegate, WKNavigationDelegate, WKUIDelegate>
|
|
41
42
|
@property(nonatomic, strong) NSWindow *window;
|
|
43
|
+
@property(nonatomic, strong) NSDictionary *guidanceCatalog;
|
|
44
|
+
@property(nonatomic, strong) NSView *guidancePanel;
|
|
45
|
+
@property(nonatomic, strong) NSTextField *guidanceTitle;
|
|
46
|
+
@property(nonatomic, strong) NSTextField *guidanceBody;
|
|
47
|
+
@property(nonatomic, strong) NSButton *guidanceToggle;
|
|
48
|
+
@property(nonatomic, strong) NSButton *guidanceRestore;
|
|
49
|
+
@property(nonatomic, strong) NSMutableSet<NSString *> *dismissedGuidance;
|
|
50
|
+
|
|
51
|
+
@property(nonatomic, strong) NSFileHandle *setupOutput;
|
|
52
|
+
@property(nonatomic, strong) NSMutableData *setupOutputBuffer;
|
|
53
|
+
@property(nonatomic, strong) NSWindow *cameraWindow;
|
|
54
|
+
@property(nonatomic, strong) WKWebView *cameraWebView;
|
|
55
|
+
@property(nonatomic, strong) NSWindow *legalWindow;
|
|
56
|
+
@property(nonatomic, strong) WKWebView *legalWebView;
|
|
42
57
|
@property(nonatomic, strong) NSTextField *headline;
|
|
43
58
|
@property(nonatomic, strong) NSTextField *detail;
|
|
44
59
|
@property(nonatomic, strong) NSTextField *statusValue;
|
|
@@ -163,6 +178,11 @@ static BOOL acquireSetupInstanceLock(void) {
|
|
|
163
178
|
- (void)stopPairProcess {
|
|
164
179
|
pid_t pid = self.pairProcessIdentifier;
|
|
165
180
|
self.pairProcessIdentifier = 0;
|
|
181
|
+
if (self.setupOutput) {
|
|
182
|
+
self.setupOutput.readabilityHandler = nil;
|
|
183
|
+
[self.setupOutput closeFile];
|
|
184
|
+
self.setupOutput = nil;
|
|
185
|
+
}
|
|
166
186
|
if (self.setupInput) {
|
|
167
187
|
[self.setupInput closeFile];
|
|
168
188
|
self.setupInput = nil;
|
|
@@ -206,9 +226,18 @@ static BOOL acquireSetupInstanceLock(void) {
|
|
|
206
226
|
return NO;
|
|
207
227
|
}
|
|
208
228
|
|
|
229
|
+
int outputPipe[2];
|
|
230
|
+
if (pipe(outputPipe) != 0) {
|
|
231
|
+
close(logFd); close(inputPipe[0]); close(inputPipe[1]);
|
|
232
|
+
if (error) *error = [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:nil];
|
|
233
|
+
return NO;
|
|
234
|
+
}
|
|
209
235
|
posix_spawn_file_actions_t fileActions;
|
|
210
236
|
posix_spawnattr_t attributes;
|
|
211
237
|
posix_spawn_file_actions_init(&fileActions);
|
|
238
|
+
posix_spawn_file_actions_adddup2(&fileActions, outputPipe[1], 198);
|
|
239
|
+
posix_spawn_file_actions_addclose(&fileActions, outputPipe[0]);
|
|
240
|
+
posix_spawn_file_actions_addclose(&fileActions, outputPipe[1]);
|
|
212
241
|
posix_spawn_file_actions_adddup2(&fileActions, logFd, STDOUT_FILENO);
|
|
213
242
|
posix_spawn_file_actions_adddup2(&fileActions, logFd, STDERR_FILENO);
|
|
214
243
|
posix_spawn_file_actions_adddup2(&fileActions, inputPipe[0], STDIN_FILENO);
|
|
@@ -239,12 +268,24 @@ static BOOL acquireSetupInstanceLock(void) {
|
|
|
239
268
|
posix_spawn_file_actions_destroy(&fileActions);
|
|
240
269
|
close(logFd);
|
|
241
270
|
close(inputPipe[0]);
|
|
271
|
+
close(outputPipe[1]);
|
|
242
272
|
if (spawnError != 0) {
|
|
243
273
|
close(inputPipe[1]);
|
|
274
|
+
close(outputPipe[0]);
|
|
244
275
|
if (error) *error = [NSError errorWithDomain:NSPOSIXErrorDomain code:spawnError userInfo:nil];
|
|
245
276
|
return NO;
|
|
246
277
|
}
|
|
247
278
|
|
|
279
|
+
self.setupOutputBuffer = [NSMutableData data];
|
|
280
|
+
self.setupOutput = [[NSFileHandle alloc] initWithFileDescriptor:outputPipe[0] closeOnDealloc:YES];
|
|
281
|
+
__weak typeof(self) outputSelf = self;
|
|
282
|
+
self.setupOutput.readabilityHandler = ^(NSFileHandle *handle) {
|
|
283
|
+
NSData *data = handle.availableData;
|
|
284
|
+
if (data.length == 0) { handle.readabilityHandler = nil; return; }
|
|
285
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
286
|
+
if (outputSelf.setupOutput == handle) [outputSelf receiveSetupOutput:data];
|
|
287
|
+
});
|
|
288
|
+
};
|
|
248
289
|
self.setupInput = [[NSFileHandle alloc] initWithFileDescriptor:inputPipe[1] closeOnDealloc:YES];
|
|
249
290
|
self.pairProcessIdentifier = pid;
|
|
250
291
|
dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_PROC,
|
|
@@ -265,6 +306,11 @@ static BOOL acquireSetupInstanceLock(void) {
|
|
|
265
306
|
int status = 0;
|
|
266
307
|
pid_t waited = waitpid(pid, &status, 0);
|
|
267
308
|
self.pairProcessIdentifier = 0;
|
|
309
|
+
if (self.setupOutput) {
|
|
310
|
+
self.setupOutput.readabilityHandler = nil;
|
|
311
|
+
[self.setupOutput closeFile];
|
|
312
|
+
self.setupOutput = nil;
|
|
313
|
+
}
|
|
268
314
|
if (self.setupInput) {
|
|
269
315
|
[self.setupInput closeFile];
|
|
270
316
|
self.setupInput = nil;
|
|
@@ -289,13 +335,14 @@ static BOOL acquireSetupInstanceLock(void) {
|
|
|
289
335
|
|
|
290
336
|
- (void)buildWindow {
|
|
291
337
|
self.window = [[NSWindow alloc]
|
|
292
|
-
initWithContentRect:NSMakeRect(0, 0, 760,
|
|
338
|
+
initWithContentRect:NSMakeRect(0, 0, 760, 700)
|
|
293
339
|
styleMask:NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable
|
|
294
340
|
backing:NSBackingStoreBuffered
|
|
295
341
|
defer:NO];
|
|
296
342
|
self.window.title = @"Sense Anything Connector";
|
|
297
|
-
self.window.
|
|
298
|
-
self.window.
|
|
343
|
+
self.window.delegate = self;
|
|
344
|
+
self.window.minSize = NSMakeSize(760, 700);
|
|
345
|
+
self.window.maxSize = NSMakeSize(760, 700);
|
|
299
346
|
[self.window center];
|
|
300
347
|
|
|
301
348
|
NSView *root = self.window.contentView;
|
|
@@ -341,7 +388,7 @@ static BOOL acquireSetupInstanceLock(void) {
|
|
|
341
388
|
[root addSubview:self.statusValue];
|
|
342
389
|
|
|
343
390
|
self.googleReopenButton = [[NSButton alloc] initWithFrame:NSMakeRect(360, 219, 218, 28)];
|
|
344
|
-
self.googleReopenButton.title = @"
|
|
391
|
+
self.googleReopenButton.title = @"Reopen setup tab";
|
|
345
392
|
self.googleReopenButton.bezelStyle = NSBezelStyleRounded;
|
|
346
393
|
self.googleReopenButton.target = self;
|
|
347
394
|
self.googleReopenButton.action = @selector(googleReopenPressed);
|
|
@@ -497,10 +544,85 @@ static BOOL acquireSetupInstanceLock(void) {
|
|
|
497
544
|
[self.chooseAccountPanel addSubview:self.chooseAccountSubmit];
|
|
498
545
|
[root addSubview:self.chooseAccountPanel];
|
|
499
546
|
|
|
547
|
+
// Reserve a separate help region; never cover native forms or recovery actions.
|
|
548
|
+
for (NSView *view in root.subviews) {
|
|
549
|
+
NSRect frame = view.frame;
|
|
550
|
+
frame.origin.y += 160;
|
|
551
|
+
view.frame = frame;
|
|
552
|
+
}
|
|
553
|
+
NSView *guideSidebar = [[NSView alloc] initWithFrame:NSMakeRect(0, 0, 190, 160)];
|
|
554
|
+
guideSidebar.wantsLayer = YES;
|
|
555
|
+
guideSidebar.layer.backgroundColor = NSColor.controlBackgroundColor.CGColor;
|
|
556
|
+
[root addSubview:guideSidebar];
|
|
557
|
+
NSString *guidePath = [NSBundle.mainBundle pathForResource:@"setup-guidance" ofType:@"json"];
|
|
558
|
+
NSData *guideData = guidePath ? [NSData dataWithContentsOfFile:guidePath] : nil;
|
|
559
|
+
id catalog = guideData ? [NSJSONSerialization JSONObjectWithData:guideData options:0 error:nil] : nil;
|
|
560
|
+
self.guidanceCatalog = [catalog isKindOfClass:NSDictionary.class] ? catalog : @{};
|
|
561
|
+
self.dismissedGuidance = [NSMutableSet set];
|
|
562
|
+
self.guidanceToggle = [NSButton checkboxWithTitle:@"Guiding tips" target:self action:@selector(toggleGuidance)];
|
|
563
|
+
self.guidanceToggle.frame = NSMakeRect(24, 118, 150, 24);
|
|
564
|
+
self.guidanceToggle.state = [NSUserDefaults.standardUserDefaults boolForKey:@"setupGuidingTipsDisabled"] ? NSControlStateValueOff : NSControlStateValueOn;
|
|
565
|
+
[root addSubview:self.guidanceToggle];
|
|
566
|
+
self.guidanceRestore = [self panelButton:@"Show this tip" frame:NSMakeRect(20, 80, 150, 28) action:@selector(restoreGuidance)];
|
|
567
|
+
[root addSubview:self.guidanceRestore];
|
|
568
|
+
self.guidancePanel = [[NSView alloc] initWithFrame:NSMakeRect(236, 12, 465, 145)];
|
|
569
|
+
self.guidancePanel.wantsLayer = YES;
|
|
570
|
+
self.guidancePanel.layer.backgroundColor = [NSColor colorWithCalibratedRed:1 green:0.96 blue:0.91 alpha:1].CGColor;
|
|
571
|
+
self.guidancePanel.layer.borderColor = [NSColor colorWithCalibratedRed:0.93 green:0.65 blue:0.40 alpha:1].CGColor;
|
|
572
|
+
self.guidancePanel.layer.borderWidth = 1;
|
|
573
|
+
self.guidancePanel.layer.cornerRadius = 10;
|
|
574
|
+
self.guidanceTitle = [self label:@"" size:13 weight:NSFontWeightSemibold frame:NSMakeRect(14, 94, 405, 40)];
|
|
575
|
+
self.guidanceBody = [self label:@"" size:12 weight:NSFontWeightRegular frame:NSMakeRect(14, 35, 435, 60)];
|
|
576
|
+
self.guidanceTitle.textColor = [NSColor colorWithCalibratedRed:0.28 green:0.20 blue:0.14 alpha:1];
|
|
577
|
+
self.guidanceBody.textColor = [NSColor colorWithCalibratedRed:0.38 green:0.29 blue:0.23 alpha:1];
|
|
578
|
+
[self.guidancePanel addSubview:self.guidanceTitle];
|
|
579
|
+
[self.guidancePanel addSubview:self.guidanceBody];
|
|
580
|
+
NSButton *dismiss = [self panelButton:@"×" frame:NSMakeRect(424, 110, 30, 26) action:@selector(dismissGuidance)];
|
|
581
|
+
dismiss.accessibilityLabel = @"Dismiss this tip";
|
|
582
|
+
[self.guidancePanel addSubview:dismiss];
|
|
583
|
+
[self.guidancePanel addSubview:[self panelButton:@"More help" frame:NSMakeRect(8, 5, 110, 26) action:@selector(showGuidanceDetail)]];
|
|
584
|
+
[root addSubview:self.guidancePanel];
|
|
585
|
+
|
|
500
586
|
self.onboardingPanels = @[self.accountPanel, self.termsPanel, self.planPanel, self.chooseAccountPanel];
|
|
501
587
|
[self showOnboardingPanel:nil];
|
|
502
588
|
}
|
|
503
589
|
|
|
590
|
+
- (void)renderGuidance {
|
|
591
|
+
NSDictionary *guide = self.guidanceCatalog[self.lastStage];
|
|
592
|
+
BOOL enabled = self.guidanceToggle.state == NSControlStateValueOn;
|
|
593
|
+
BOOL dismissed = [self.dismissedGuidance containsObject:self.lastStage];
|
|
594
|
+
self.guidancePanel.hidden = !enabled || dismissed || ![guide isKindOfClass:NSDictionary.class];
|
|
595
|
+
self.guidanceRestore.hidden = !enabled || !dismissed;
|
|
596
|
+
self.guidanceTitle.stringValue = [guide[@"title"] isKindOfClass:NSString.class] ? guide[@"title"] : @"";
|
|
597
|
+
self.guidanceBody.stringValue = [guide[@"body"] isKindOfClass:NSString.class] ? guide[@"body"] : @"";
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
- (void)toggleGuidance {
|
|
601
|
+
[NSUserDefaults.standardUserDefaults setBool:self.guidanceToggle.state != NSControlStateValueOn forKey:@"setupGuidingTipsDisabled"];
|
|
602
|
+
[self.dismissedGuidance removeAllObjects];
|
|
603
|
+
[self renderGuidance];
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
- (void)dismissGuidance {
|
|
607
|
+
[self.dismissedGuidance addObject:self.lastStage];
|
|
608
|
+
[self renderGuidance];
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
- (void)restoreGuidance {
|
|
612
|
+
[self.dismissedGuidance removeObject:self.lastStage];
|
|
613
|
+
[self renderGuidance];
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
- (void)showGuidanceDetail {
|
|
617
|
+
NSDictionary *guide = self.guidanceCatalog[self.lastStage];
|
|
618
|
+
if (![guide[@"detail"] isKindOfClass:NSString.class]) return;
|
|
619
|
+
NSAlert *alert = [[NSAlert alloc] init];
|
|
620
|
+
alert.messageText = self.guidanceTitle.stringValue;
|
|
621
|
+
alert.informativeText = guide[@"detail"];
|
|
622
|
+
[alert addButtonWithTitle:@"Got it"];
|
|
623
|
+
[alert beginSheetModalForWindow:self.window completionHandler:nil];
|
|
624
|
+
}
|
|
625
|
+
|
|
504
626
|
- (void)showOnboardingPanel:(NSView *)panel {
|
|
505
627
|
for (NSView *candidate in self.onboardingPanels ?: @[]) candidate.hidden = candidate != panel;
|
|
506
628
|
BOOL showStatus = panel == nil;
|
|
@@ -537,7 +659,7 @@ static BOOL acquireSetupInstanceLock(void) {
|
|
|
537
659
|
}
|
|
538
660
|
|
|
539
661
|
- (void)googleReopenPressed {
|
|
540
|
-
[self sendOnboardingCommand:@{ @"action": @"
|
|
662
|
+
[self sendOnboardingCommand:@{ @"action": @"browser_reopen" }];
|
|
541
663
|
}
|
|
542
664
|
|
|
543
665
|
- (void)googlePressed {
|
|
@@ -586,15 +708,15 @@ static BOOL acquireSetupInstanceLock(void) {
|
|
|
586
708
|
}
|
|
587
709
|
|
|
588
710
|
- (void)reviewTermsPressed {
|
|
589
|
-
[self
|
|
711
|
+
[self showLegalURL:@"https://trillboards.com/legal/terms"];
|
|
590
712
|
}
|
|
591
713
|
|
|
592
714
|
- (void)reviewPrivacyPressed {
|
|
593
|
-
[self
|
|
715
|
+
[self showLegalURL:@"https://trillboards.com/legal/privacy-policy"];
|
|
594
716
|
}
|
|
595
717
|
|
|
596
718
|
- (void)reviewDpaPressed {
|
|
597
|
-
[self
|
|
719
|
+
[self showLegalURL:@"https://trillboards.com/legal/data-processing"];
|
|
598
720
|
}
|
|
599
721
|
|
|
600
722
|
- (void)termsSubmitPressed {
|
|
@@ -624,6 +746,7 @@ static BOOL acquireSetupInstanceLock(void) {
|
|
|
624
746
|
- (void)showWelcome {
|
|
625
747
|
[self showOnboardingPanel:nil];
|
|
626
748
|
self.lastStage = @"welcome";
|
|
749
|
+
[self renderGuidance];
|
|
627
750
|
self.headline.stringValue = @"Set up this computer for CCTV";
|
|
628
751
|
self.detail.stringValue = @"Connect your Sense Anything account, find your recorder, and verify every camera before screens are created.";
|
|
629
752
|
self.statusValue.stringValue = @"About 5 minutes";
|
|
@@ -639,6 +762,7 @@ static BOOL acquireSetupInstanceLock(void) {
|
|
|
639
762
|
- (void)showLocalError:(NSString *)title message:(NSString *)message {
|
|
640
763
|
[self showOnboardingPanel:nil];
|
|
641
764
|
self.lastStage = @"error";
|
|
765
|
+
[self renderGuidance];
|
|
642
766
|
self.headline.stringValue = title;
|
|
643
767
|
self.detail.stringValue = message;
|
|
644
768
|
self.statusValue.stringValue = @"Setup stopped";
|
|
@@ -651,6 +775,14 @@ static BOOL acquireSetupInstanceLock(void) {
|
|
|
651
775
|
self.googleReopenButton.hidden = YES;
|
|
652
776
|
}
|
|
653
777
|
|
|
778
|
+
- (void)windowWillClose:(NSNotification *)notification {
|
|
779
|
+
if (notification.object == self.window) {
|
|
780
|
+
[self.cameraWindow close];
|
|
781
|
+
[self.legalWindow close];
|
|
782
|
+
[NSApp terminate:nil];
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
|
|
654
786
|
- (void)closePressed {
|
|
655
787
|
[self.window close];
|
|
656
788
|
}
|
|
@@ -716,6 +848,7 @@ static BOOL acquireSetupInstanceLock(void) {
|
|
|
716
848
|
}
|
|
717
849
|
self.requiredUpdateURL = downloadURL;
|
|
718
850
|
self.lastStage = @"update_required";
|
|
851
|
+
[self renderGuidance];
|
|
719
852
|
self.headline.stringValue = @"Update required before setup can continue";
|
|
720
853
|
self.detail.stringValue = @"Install the latest Sense Anything Connector, then reopen it to finish camera setup.";
|
|
721
854
|
self.statusValue.stringValue = [NSString stringWithFormat:@"Version %@", availableText];
|
|
@@ -756,9 +889,11 @@ static BOOL acquireSetupInstanceLock(void) {
|
|
|
756
889
|
[self sendOnboardingCommand:@{ @"action": @"refresh" }];
|
|
757
890
|
} else if ([self.lastStage isEqualToString:@"waiting_for_account"] ||
|
|
758
891
|
[self.lastStage isEqualToString:@"account_attention"]) {
|
|
759
|
-
[self
|
|
760
|
-
|
|
761
|
-
[self
|
|
892
|
+
[self sendOnboardingCommand:@{ @"action": @"camera_reopen" }];
|
|
893
|
+
if (self.cameraWindow) [self.cameraWindow makeKeyAndOrderFront:nil];
|
|
894
|
+
else [self showLocalError:@"Camera setup unavailable" message:@"Restart setup to reconnect the camera window to your account."];
|
|
895
|
+
} else if ([@[@"ready", @"account_connected", @"discovering", @"devices_found", @"validating"] containsObject:self.lastStage]) {
|
|
896
|
+
if (self.cameraWindow) [self.cameraWindow makeKeyAndOrderFront:nil];
|
|
762
897
|
} else if ([self.lastStage isEqualToString:@"local_ai_error"]) {
|
|
763
898
|
[NSApp terminate:nil];
|
|
764
899
|
} else if ([self.lastStage isEqualToString:@"welcome"] || [self.lastStage isEqualToString:@"error"]) {
|
|
@@ -767,6 +902,9 @@ static BOOL acquireSetupInstanceLock(void) {
|
|
|
767
902
|
}
|
|
768
903
|
|
|
769
904
|
- (void)startSetup {
|
|
905
|
+
[self stopPairProcess];
|
|
906
|
+
[self.cameraWindow close]; self.cameraWindow = nil; self.cameraWebView = nil;
|
|
907
|
+
[self.legalWindow close]; self.legalWindow = nil; self.legalWebView = nil;
|
|
770
908
|
NSString *pairer = [self homePath:@"Library/Trillboards/edge/bin/trillboards-pair"];
|
|
771
909
|
NSString *stateDirectory = [self homePath:@"Library/Application Support/Trillboards"];
|
|
772
910
|
NSString *statePath = [stateDirectory stringByAppendingPathComponent:@"setup-progress.json"];
|
|
@@ -783,6 +921,7 @@ static BOOL acquireSetupInstanceLock(void) {
|
|
|
783
921
|
if (![files fileExistsAtPath:logPath]) [files createFileAtPath:logPath contents:nil attributes:nil];
|
|
784
922
|
|
|
785
923
|
self.lastStage = @"starting";
|
|
924
|
+
[self renderGuidance];
|
|
786
925
|
self.setupStarted = YES;
|
|
787
926
|
self.headline.stringValue = @"Starting camera setup";
|
|
788
927
|
self.detail.stringValue = @"Connecting securely to Sense Anything.";
|
|
@@ -793,7 +932,7 @@ static BOOL acquireSetupInstanceLock(void) {
|
|
|
793
932
|
|
|
794
933
|
NSError *error = nil;
|
|
795
934
|
if (![self launchSetupExecutable:pairer
|
|
796
|
-
arguments:@[@"--setup", @"--status-file", statePath]
|
|
935
|
+
arguments:@[@"--setup", @"--status-file", statePath, @"--native-ui-fd", @"198"]
|
|
797
936
|
logPath:logPath
|
|
798
937
|
error:&error]) {
|
|
799
938
|
[self showLocalError:@"Could not start setup" message:@"Reinstall the Sense Anything Connector, then try again."];
|
|
@@ -816,6 +955,7 @@ static BOOL acquireSetupInstanceLock(void) {
|
|
|
816
955
|
if (!stage) return;
|
|
817
956
|
NSString *previousStage = self.lastStage;
|
|
818
957
|
self.lastStage = stage;
|
|
958
|
+
[self renderGuidance];
|
|
819
959
|
if ([state[@"portalUrl"] isKindOfClass:NSString.class]) self.portalURL = state[@"portalUrl"];
|
|
820
960
|
if ([state[@"title"] isKindOfClass:NSString.class]) self.headline.stringValue = state[@"title"];
|
|
821
961
|
if ([state[@"message"] isKindOfClass:NSString.class]) self.detail.stringValue = state[@"message"];
|
|
@@ -902,6 +1042,7 @@ static BOOL acquireSetupInstanceLock(void) {
|
|
|
902
1042
|
self.googleReopenButton.hidden = NO;
|
|
903
1043
|
} else if ([stage isEqualToString:@"onboarding_checkout"]) {
|
|
904
1044
|
self.statusValue.stringValue = @"Waiting for Stripe";
|
|
1045
|
+
self.googleReopenButton.hidden = NO;
|
|
905
1046
|
} else if ([stage isEqualToString:@"onboarding_install"]) {
|
|
906
1047
|
self.statusValue.stringValue = @"Onboarding complete";
|
|
907
1048
|
} else if (cameraCount.integerValue > 0) {
|
|
@@ -956,10 +1097,13 @@ static BOOL acquireSetupInstanceLock(void) {
|
|
|
956
1097
|
self.actionButton.title = @"Preparing cameras…";
|
|
957
1098
|
self.actionButton.enabled = NO;
|
|
958
1099
|
} else if ([stage isEqualToString:@"waiting_for_account"]) {
|
|
959
|
-
self.actionButton.title = @"Open
|
|
1100
|
+
self.actionButton.title = @"Open camera setup";
|
|
960
1101
|
self.actionButton.enabled = YES;
|
|
961
1102
|
} else if ([stage isEqualToString:@"account_attention"]) {
|
|
962
|
-
self.actionButton.title = @"
|
|
1103
|
+
self.actionButton.title = @"Open camera setup";
|
|
1104
|
+
self.actionButton.enabled = YES;
|
|
1105
|
+
} else if ([@[@"account_connected", @"discovering", @"devices_found", @"validating"] containsObject:stage] && self.cameraWindow) {
|
|
1106
|
+
self.actionButton.title = @"Open camera setup";
|
|
963
1107
|
self.actionButton.enabled = YES;
|
|
964
1108
|
} else if ([stage isEqualToString:@"local_ai_error"]) {
|
|
965
1109
|
self.actionButton.title = @"Close";
|
|
@@ -1013,6 +1157,98 @@ static BOOL acquireSetupInstanceLock(void) {
|
|
|
1013
1157
|
}
|
|
1014
1158
|
}
|
|
1015
1159
|
|
|
1160
|
+
- (void)receiveSetupOutput:(NSData *)data {
|
|
1161
|
+
if (self.terminating || !self.setupOutput) return;
|
|
1162
|
+
[self.setupOutputBuffer appendData:data];
|
|
1163
|
+
if (self.setupOutputBuffer.length > 16384) {
|
|
1164
|
+
[self.setupOutputBuffer setLength:0];
|
|
1165
|
+
[self showLocalError:@"Camera setup unavailable" message:@"Restart setup to reconnect the camera window."];
|
|
1166
|
+
return;
|
|
1167
|
+
}
|
|
1168
|
+
NSData *newline = [@"\n" dataUsingEncoding:NSUTF8StringEncoding];
|
|
1169
|
+
while (YES) {
|
|
1170
|
+
NSRange boundary = [self.setupOutputBuffer rangeOfData:newline options:0 range:NSMakeRange(0, self.setupOutputBuffer.length)];
|
|
1171
|
+
if (boundary.location == NSNotFound) break;
|
|
1172
|
+
NSData *line = [self.setupOutputBuffer subdataWithRange:NSMakeRange(0, boundary.location)];
|
|
1173
|
+
[self.setupOutputBuffer replaceBytesInRange:NSMakeRange(0, boundary.location + 1) withBytes:NULL length:0];
|
|
1174
|
+
NSDictionary *message = [NSJSONSerialization JSONObjectWithData:line options:0 error:nil];
|
|
1175
|
+
if (![message isKindOfClass:NSDictionary.class] || ![message[@"action"] isEqual:@"camera"] || ![message[@"url"] isKindOfClass:NSString.class]) continue;
|
|
1176
|
+
NSURL *url = [NSURL URLWithString:message[@"url"]];
|
|
1177
|
+
if (![url.scheme isEqual:@"https"] || ![url.host isEqual:@"app.senseanything.com"] || ![url.path isEqual:@"/sense/installer-return"]) {
|
|
1178
|
+
[self showLocalError:@"Camera setup unavailable" message:@"The camera setup destination was not recognized."];
|
|
1179
|
+
continue;
|
|
1180
|
+
}
|
|
1181
|
+
if (!self.cameraWebView) {
|
|
1182
|
+
self.cameraWebView = [self setupWebView];
|
|
1183
|
+
self.cameraWindow = [self contentWindow:@"Sense Anything — Recorder and cameras" webView:self.cameraWebView];
|
|
1184
|
+
}
|
|
1185
|
+
[self.cameraWebView loadRequest:[NSURLRequest requestWithURL:url]];
|
|
1186
|
+
[self.cameraWindow makeKeyAndOrderFront:nil];
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
- (WKWebView *)setupWebView {
|
|
1191
|
+
WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc] init];
|
|
1192
|
+
configuration.websiteDataStore = WKWebsiteDataStore.nonPersistentDataStore;
|
|
1193
|
+
WKWebView *view = [[WKWebView alloc] initWithFrame:NSMakeRect(0, 0, 960, 700) configuration:configuration];
|
|
1194
|
+
view.navigationDelegate = self;
|
|
1195
|
+
view.UIDelegate = self;
|
|
1196
|
+
return view;
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
- (NSWindow *)contentWindow:(NSString *)title webView:(WKWebView *)view {
|
|
1200
|
+
NSWindow *window = [[NSWindow alloc] initWithContentRect:NSMakeRect(0, 0, 960, 700)
|
|
1201
|
+
styleMask:NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskResizable
|
|
1202
|
+
backing:NSBackingStoreBuffered defer:NO];
|
|
1203
|
+
window.title = title;
|
|
1204
|
+
window.releasedWhenClosed = NO;
|
|
1205
|
+
window.contentView = view;
|
|
1206
|
+
// An owned window stays with the installer rather than activating whichever
|
|
1207
|
+
// unrelated browser window happens to be on another desktop.
|
|
1208
|
+
[self.window addChildWindow:window ordered:NSWindowAbove];
|
|
1209
|
+
NSRect visible = (self.window.screen ?: NSScreen.mainScreen).visibleFrame;
|
|
1210
|
+
NSRect frame = window.frame;
|
|
1211
|
+
frame.size.width = MIN(frame.size.width, NSWidth(visible) - 40);
|
|
1212
|
+
frame.size.height = MIN(frame.size.height, NSHeight(visible) - 40);
|
|
1213
|
+
frame.origin.x = MAX(NSMinX(visible) + 20, MIN(NSMinX(self.window.frame) + 30, NSMaxX(visible) - NSWidth(frame) - 20));
|
|
1214
|
+
frame.origin.y = MAX(NSMinY(visible) + 20, MIN(NSMinY(self.window.frame) + 20, NSMaxY(visible) - NSHeight(frame) - 20));
|
|
1215
|
+
[window setFrame:frame display:NO];
|
|
1216
|
+
return window;
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
- (void)showLegalURL:(NSString *)string {
|
|
1220
|
+
if (!self.legalWebView) {
|
|
1221
|
+
self.legalWebView = [self setupWebView];
|
|
1222
|
+
self.legalWindow = [self contentWindow:@"Sense Anything — Agreements" webView:self.legalWebView];
|
|
1223
|
+
}
|
|
1224
|
+
[self.legalWebView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:string]]];
|
|
1225
|
+
[self.legalWindow makeKeyAndOrderFront:nil];
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)action
|
|
1229
|
+
decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
|
|
1230
|
+
NSURL *url = action.request.URL;
|
|
1231
|
+
BOOL allowedHost = webView == self.cameraWebView
|
|
1232
|
+
? [url.host isEqual:@"app.senseanything.com"]
|
|
1233
|
+
: ([url.host isEqual:@"trillboards.com"] || [url.host isEqual:@"www.trillboards.com"]);
|
|
1234
|
+
BOOL allowed = [url.scheme isEqual:@"https"] && allowedHost;
|
|
1235
|
+
if (allowed && !action.targetFrame) {
|
|
1236
|
+
[webView loadRequest:action.request];
|
|
1237
|
+
decisionHandler(WKNavigationActionPolicyCancel);
|
|
1238
|
+
return;
|
|
1239
|
+
}
|
|
1240
|
+
decisionHandler(allowed ? WKNavigationActionPolicyAllow : WKNavigationActionPolicyCancel);
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
- (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(WKNavigation *)navigation withError:(NSError *)error {
|
|
1244
|
+
(void)webView; (void)navigation;
|
|
1245
|
+
if (error.code == NSURLErrorCancelled) return;
|
|
1246
|
+
NSAlert *alert = [[NSAlert alloc] init];
|
|
1247
|
+
alert.messageText = @"Could not load this setup page";
|
|
1248
|
+
alert.informativeText = @"Check your internet connection, then reopen the page from the Connector.";
|
|
1249
|
+
[alert beginSheetModalForWindow:self.window completionHandler:nil];
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1016
1252
|
- (void)openURL:(NSString *)string {
|
|
1017
1253
|
NSURL *url = [NSURL URLWithString:string];
|
|
1018
1254
|
if (url) [NSWorkspace.sharedWorkspace openURL:url];
|
|
@@ -1020,6 +1256,43 @@ static BOOL acquireSetupInstanceLock(void) {
|
|
|
1020
1256
|
|
|
1021
1257
|
@end
|
|
1022
1258
|
|
|
1259
|
+
static int runGuidanceSmokeTest(void) {
|
|
1260
|
+
[NSApplication sharedApplication];
|
|
1261
|
+
TrillboardsSetupDelegate *delegate = [[TrillboardsSetupDelegate alloc] init];
|
|
1262
|
+
[delegate buildWindow];
|
|
1263
|
+
if (delegate.guidanceCatalog.count == 0) return 1;
|
|
1264
|
+
delegate.guidanceToggle.state = NSControlStateValueOn;
|
|
1265
|
+
for (NSString *stage in delegate.guidanceCatalog) {
|
|
1266
|
+
delegate.lastStage = stage;
|
|
1267
|
+
[delegate renderGuidance];
|
|
1268
|
+
if (delegate.guidancePanel.hidden || !delegate.guidanceTitle.stringValue.length) return 2;
|
|
1269
|
+
for (NSTextField *field in @[delegate.guidanceTitle, delegate.guidanceBody]) {
|
|
1270
|
+
NSRect textBounds = [field.attributedStringValue boundingRectWithSize:NSMakeSize(field.frame.size.width - 4, CGFLOAT_MAX)
|
|
1271
|
+
options:NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading];
|
|
1272
|
+
if (textBounds.size.height > field.frame.size.height) {
|
|
1273
|
+
fprintf(stderr, "Guidance text clipped at %s\n", stage.UTF8String);
|
|
1274
|
+
return 3;
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
[delegate dismissGuidance];
|
|
1278
|
+
if (!delegate.guidancePanel.hidden || delegate.guidanceRestore.hidden) return 4;
|
|
1279
|
+
[delegate restoreGuidance];
|
|
1280
|
+
if (delegate.guidancePanel.hidden) return 5;
|
|
1281
|
+
}
|
|
1282
|
+
delegate.lastStage = @"onboarding_terms";
|
|
1283
|
+
delegate.termsCheck.state = NSControlStateValueOff;
|
|
1284
|
+
delegate.termsSubmit.enabled = NO;
|
|
1285
|
+
delegate.guidanceToggle.state = NSControlStateValueOff;
|
|
1286
|
+
[delegate renderGuidance];
|
|
1287
|
+
if (!delegate.guidancePanel.hidden || delegate.termsSubmit.enabled) return 6;
|
|
1288
|
+
if (NSIntersectsRect(delegate.guidancePanel.frame, delegate.accountPanel.frame) ||
|
|
1289
|
+
NSIntersectsRect(delegate.guidancePanel.frame, delegate.actionButton.frame)) return 7;
|
|
1290
|
+
fprintf(stdout, "Native guidance stages, dismissal, text bounds and consent isolation passed.\n");
|
|
1291
|
+
fflush(stdout);
|
|
1292
|
+
[delegate.window close];
|
|
1293
|
+
return 0;
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1023
1296
|
static int runCancellationSmokeTest(void) {
|
|
1024
1297
|
NSString *pidPath = [NSTemporaryDirectory()
|
|
1025
1298
|
stringByAppendingPathComponent:[NSString stringWithFormat:@"trillboards-setup-child-%@.pid", NSUUID.UUID.UUIDString]];
|
|
@@ -1068,6 +1341,35 @@ static int runCancellationSmokeTest(void) {
|
|
|
1068
1341
|
return 0;
|
|
1069
1342
|
}
|
|
1070
1343
|
|
|
1344
|
+
@interface SetupPipeSmokeDelegate : TrillboardsSetupDelegate
|
|
1345
|
+
@property(nonatomic, strong) NSMutableData *received;
|
|
1346
|
+
@end
|
|
1347
|
+
@implementation SetupPipeSmokeDelegate
|
|
1348
|
+
- (void)receiveSetupOutput:(NSData *)data { [self.received appendData:data]; }
|
|
1349
|
+
@end
|
|
1350
|
+
|
|
1351
|
+
static int runNativePipeSmokeTest(NSString *nodePath) {
|
|
1352
|
+
SetupPipeSmokeDelegate *delegate = [[SetupPipeSmokeDelegate alloc] init];
|
|
1353
|
+
delegate.received = [NSMutableData data];
|
|
1354
|
+
NSError *error = nil;
|
|
1355
|
+
if (![delegate launchSetupExecutable:@"/bin/bash"
|
|
1356
|
+
arguments:@[@"-c", @"exec \"$1\" -e \"$2\"", @"pipe-smoke", nodePath,
|
|
1357
|
+
@"require('fs').writeSync(198, 'native-ui-pipe-ok\\n'); setTimeout(() => {}, 10000)"]
|
|
1358
|
+
logPath:@"/dev/null" error:&error]) return 1;
|
|
1359
|
+
NSDate *deadline = [NSDate dateWithTimeIntervalSinceNow:3];
|
|
1360
|
+
while (delegate.received.length == 0 && deadline.timeIntervalSinceNow > 0) {
|
|
1361
|
+
[NSRunLoop.currentRunLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.02]];
|
|
1362
|
+
}
|
|
1363
|
+
NSString *received = [[NSString alloc] initWithData:delegate.received encoding:NSUTF8StringEncoding];
|
|
1364
|
+
[delegate stopPairProcess];
|
|
1365
|
+
if (![received isEqualToString:@"native-ui-pipe-ok\n"]) {
|
|
1366
|
+
fprintf(stderr, "Native setup private UI pipe did not deliver its message.\n");
|
|
1367
|
+
return 1;
|
|
1368
|
+
}
|
|
1369
|
+
fprintf(stdout, "Sense Anything Connector private UI pipe smoke test passed.\n");
|
|
1370
|
+
return 0;
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1071
1373
|
static int runTemporaryDirectorySmokeTest(void) {
|
|
1072
1374
|
const char *environmentTemporaryDirectory = getenv("TMPDIR");
|
|
1073
1375
|
if (environmentTemporaryDirectory != NULL &&
|
|
@@ -1108,6 +1410,12 @@ int main(int argc, const char *argv[]) {
|
|
|
1108
1410
|
// setup app or its pairing child retain that now-stale TMPDIR.
|
|
1109
1411
|
normalizeTemporaryDirectory();
|
|
1110
1412
|
@autoreleasepool {
|
|
1413
|
+
if (argc == 3 && [[NSString stringWithUTF8String:argv[1]] isEqualToString:@"--smoke-test-ui-pipe"]) {
|
|
1414
|
+
return runNativePipeSmokeTest([NSString stringWithUTF8String:argv[2]]);
|
|
1415
|
+
}
|
|
1416
|
+
if (argc == 2 && [[NSString stringWithUTF8String:argv[1]] isEqualToString:@"--smoke-test-guidance"]) {
|
|
1417
|
+
return runGuidanceSmokeTest();
|
|
1418
|
+
}
|
|
1111
1419
|
if (argc == 2 && [[NSString stringWithUTF8String:argv[1]] isEqualToString:@"--smoke-test-cancel"]) {
|
|
1112
1420
|
return runCancellationSmokeTest();
|
|
1113
1421
|
}
|
|
@@ -490,9 +490,10 @@ SETUP_APP_BIN="$SETUP_APP_MACOS/Sense Anything Connector"
|
|
|
490
490
|
mkdir -p "$SETUP_APP_MACOS" "$SETUP_APP_RESOURCES"
|
|
491
491
|
cp "$PKG_ROOT/deploy/branding/SenseAnythingConnector.icns" \
|
|
492
492
|
"$SETUP_APP_RESOURCES/SenseAnythingConnector.icns"
|
|
493
|
+
cp "$PKG_ROOT/deploy/setup-guidance.json" "$SETUP_APP_RESOURCES/setup-guidance.json"
|
|
493
494
|
log "Building native Sense Anything Connector.app…"
|
|
494
495
|
if [ "$UNIVERSAL" -eq 1 ]; then
|
|
495
|
-
clang -fobjc-arc -Wall -Wextra -Werror -O2 -mmacosx-version-min=12.0 -arch arm64 -arch x86_64 -framework Cocoa \
|
|
496
|
+
clang -fobjc-arc -Wall -Wextra -Werror -O2 -mmacosx-version-min=12.0 -arch arm64 -arch x86_64 -framework Cocoa -framework WebKit \
|
|
496
497
|
"$PKG_ROOT/deploy/macos/TrillboardsSetupApp.m" -o "$SETUP_APP_BIN" \
|
|
497
498
|
|| die "native setup app universal build failed"
|
|
498
499
|
else
|
|
@@ -500,13 +501,15 @@ else
|
|
|
500
501
|
arm64) SETUP_ARCH="arm64";;
|
|
501
502
|
x64) SETUP_ARCH="x86_64";;
|
|
502
503
|
esac
|
|
503
|
-
clang -fobjc-arc -Wall -Wextra -Werror -O2 -mmacosx-version-min=12.0 -arch "$SETUP_ARCH" -framework Cocoa \
|
|
504
|
+
clang -fobjc-arc -Wall -Wextra -Werror -O2 -mmacosx-version-min=12.0 -arch "$SETUP_ARCH" -framework Cocoa -framework WebKit \
|
|
504
505
|
"$PKG_ROOT/deploy/macos/TrillboardsSetupApp.m" -o "$SETUP_APP_BIN" \
|
|
505
506
|
|| die "native setup app build failed"
|
|
506
507
|
fi
|
|
507
508
|
chmod +x "$SETUP_APP_BIN"
|
|
508
509
|
"$SETUP_APP_BIN" --smoke-test-cancel \
|
|
509
510
|
|| die "native setup app cancellation smoke test failed"
|
|
511
|
+
"$SETUP_APP_BIN" --smoke-test-ui-pipe "$PAYLOAD/bin/node" \
|
|
512
|
+
|| die "native setup private UI pipe smoke test failed"
|
|
510
513
|
INSTALLER_TMP="$STAGE/PKInstallSandbox.trillboards-test/tmp"
|
|
511
514
|
mkdir -p "$INSTALLER_TMP"
|
|
512
515
|
TMPDIR="$INSTALLER_TMP" "$SETUP_APP_BIN" --smoke-test-tempdir \
|
|
@@ -534,6 +537,8 @@ cat > "$SETUP_APP/Contents/Info.plist" <<PLIST
|
|
|
534
537
|
</plist>
|
|
535
538
|
PLIST
|
|
536
539
|
plutil -lint "$SETUP_APP/Contents/Info.plist" >/dev/null || die "native setup app Info.plist is invalid"
|
|
540
|
+
"$SETUP_APP_BIN" --smoke-test-guidance \
|
|
541
|
+
|| die "native setup guidance smoke test failed"
|
|
537
542
|
file "$SETUP_APP_BIN" | grep -q 'Mach-O' || die "native setup app executable is not Mach-O"
|
|
538
543
|
log "Native setup app staged: $SETUP_APP"
|
|
539
544
|
|
|
@@ -41,6 +41,7 @@ LABEL="com.trillboards.edge"
|
|
|
41
41
|
API_BASE="${TRILLBOARDS_API_BASE:-}"
|
|
42
42
|
HEADLESS_CODE=""
|
|
43
43
|
UI_STATUS_FILE=""
|
|
44
|
+
NATIVE_UI_ARGS=()
|
|
44
45
|
|
|
45
46
|
# Parse args. First non-flag positional == the code (headless mode).
|
|
46
47
|
while [ $# -gt 0 ]; do
|
|
@@ -48,6 +49,7 @@ while [ $# -gt 0 ]; do
|
|
|
48
49
|
--api-base) API_BASE="$2"; shift 2;;
|
|
49
50
|
--setup) SETUP_MODE=1; shift;;
|
|
50
51
|
--status-file) UI_STATUS_FILE="$2"; shift 2;;
|
|
52
|
+
--native-ui-fd) NATIVE_UI_ARGS=(--native-ui-fd "$2"); shift 2;;
|
|
51
53
|
-h|--help) sed -n '2,20p' "${BASH_SOURCE[0]}"; exit 0;;
|
|
52
54
|
*) if [ -z "$HEADLESS_CODE" ]; then HEADLESS_CODE="$1"; fi; shift;;
|
|
53
55
|
esac
|
|
@@ -368,7 +370,7 @@ if [ "${SETUP_MODE:-0}" = "1" ]; then
|
|
|
368
370
|
# says legal + entitlement are current, so cameras cannot open early.
|
|
369
371
|
# The same process keeps the authenticated session in memory through camera
|
|
370
372
|
# setup and mints a one-use signed-in browser handoff at first launch.
|
|
371
|
-
"$LAUNCHER" installer-onboarding --config "$CONFIG" --status-file "$UI_STATUS_FILE"
|
|
373
|
+
"$LAUNCHER" installer-onboarding --config "$CONFIG" --status-file "$UI_STATUS_FILE" "${NATIVE_UI_ARGS[@]}"
|
|
372
374
|
else
|
|
373
375
|
"$LAUNCHER" setup --config "$CONFIG"
|
|
374
376
|
fi
|