@trillboards/edge-sdk 0.2.135 → 0.2.137
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/docker/README.md +0 -11
- package/deploy/macos/README.md +7 -1
- package/deploy/macos/TrillboardsSetupApp.m +407 -14
- package/deploy/macos/pair.command +6 -1
- package/deploy/windows/trillboards-pair.cmd +7 -1
- package/deploy/windows/trillboards-setup.ps1 +349 -16
- package/dist/EdgeAgent.js +1 -1
- package/dist/InstallerOnboardingClient.d.ts +102 -0
- package/dist/InstallerOnboardingClient.d.ts.map +1 -0
- package/dist/InstallerOnboardingClient.js +219 -0
- package/dist/InstallerOnboardingClient.js.map +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +37 -0
- package/dist/cli.js.map +1 -1
- package/dist/diagnose.d.ts +1 -1
- package/dist/diagnose.js +1 -1
- package/dist/installerOnboardingMode.d.ts +19 -0
- package/dist/installerOnboardingMode.d.ts.map +1 -0
- package/dist/installerOnboardingMode.js +360 -0
- package/dist/installerOnboardingMode.js.map +1 -0
- package/dist/setupMode.d.ts.map +1 -1
- package/dist/setupMode.js +17 -5
- package/dist/setupMode.js.map +1 -1
- package/dist/setupProgress.d.ts +25 -1
- package/dist/setupProgress.d.ts.map +1 -1
- package/dist/setupProgress.js +43 -0
- package/dist/setupProgress.js.map +1 -1
- package/package.json +1 -1
package/deploy/docker/README.md
CHANGED
|
@@ -84,17 +84,6 @@ docker pull ghcr.io/trillboards/edge-sdk:openvino
|
|
|
84
84
|
docker pull ghcr.io/trillboards/edge-sdk:auto-sense # BYO-CCTV connector
|
|
85
85
|
```
|
|
86
86
|
|
|
87
|
-
**AWS ECR (private, for production fleet):**
|
|
88
|
-
|
|
89
|
-
```bash
|
|
90
|
-
aws ecr get-login-password --region us-east-1 | \
|
|
91
|
-
docker login --username AWS --password-stdin 975050306809.dkr.ecr.us-east-1.amazonaws.com
|
|
92
|
-
|
|
93
|
-
docker pull 975050306809.dkr.ecr.us-east-1.amazonaws.com/trillboards-edge-sdk:latest
|
|
94
|
-
docker pull 975050306809.dkr.ecr.us-east-1.amazonaws.com/trillboards-edge-sdk:cuda
|
|
95
|
-
docker pull 975050306809.dkr.ecr.us-east-1.amazonaws.com/trillboards-edge-sdk:openvino
|
|
96
|
-
```
|
|
97
|
-
|
|
98
87
|
## Quick Start
|
|
99
88
|
|
|
100
89
|
```bash
|
package/deploy/macos/README.md
CHANGED
|
@@ -46,7 +46,13 @@ delivered **only by pairing** — it is never baked into the `.pkg`.
|
|
|
46
46
|
CurrentUserHomeDirectory`.)
|
|
47
47
|
3. The installer installs and verifies the connector and included local AI
|
|
48
48
|
model. Only then does **Sense Anything Connector** open automatically. Select
|
|
49
|
-
**Start setup**.
|
|
49
|
+
**Start setup**. Follow **Welcome → Account → Account setup → Plan & Payment**.
|
|
50
|
+
Sign in with Google or email, choose the account if you manage more than one,
|
|
51
|
+
accept the current agreements, then choose a plan
|
|
52
|
+
and finish secure Stripe checkout. Cancelling checkout preserves account setup;
|
|
53
|
+
use **Back to plans** in the Connector to retry. Existing subscribed accounts
|
|
54
|
+
resume at recorder setup. The Connector checks subscription access automatically
|
|
55
|
+
before opening the signed-in Sense Anything camera wizard,
|
|
50
56
|
finds recorders on the local network, and validates each channel before any
|
|
51
57
|
camera screens are created. Black, disconnected, and unreadable inputs are
|
|
52
58
|
ignored. After the working cameras are saved, the already-installed local AI
|
|
@@ -45,11 +45,37 @@ static BOOL acquireSetupInstanceLock(void) {
|
|
|
45
45
|
@property(nonatomic, strong) NSTextField *metric;
|
|
46
46
|
@property(nonatomic, strong) NSProgressIndicator *progress;
|
|
47
47
|
@property(nonatomic, strong) NSButton *actionButton;
|
|
48
|
+
@property(nonatomic, strong) NSButton *googleReopenButton;
|
|
48
49
|
@property(nonatomic, strong) NSButton *closeButton;
|
|
49
50
|
@property(nonatomic, strong) NSButton *updateButton;
|
|
50
51
|
@property(nonatomic, strong) NSTextField *updateStatus;
|
|
52
|
+
@property(nonatomic, strong) NSTextField *privacyLabel;
|
|
51
53
|
@property(nonatomic, strong) NSArray<NSTextField *> *stepLabels;
|
|
54
|
+
@property(nonatomic, strong) NSArray<NSView *> *onboardingPanels;
|
|
55
|
+
@property(nonatomic, strong) NSView *accountPanel;
|
|
56
|
+
@property(nonatomic, strong) NSView *termsPanel;
|
|
57
|
+
@property(nonatomic, strong) NSView *chooseAccountPanel;
|
|
58
|
+
@property(nonatomic, strong) NSPopUpButton *accountPicker;
|
|
59
|
+
@property(nonatomic, strong) NSButton *chooseAccountSubmit;
|
|
60
|
+
@property(nonatomic, strong) NSView *planPanel;
|
|
61
|
+
@property(nonatomic, strong) NSTextField *nameInput;
|
|
62
|
+
@property(nonatomic, strong) NSTextField *emailInput;
|
|
63
|
+
@property(nonatomic, strong) NSSecureTextField *passwordInput;
|
|
64
|
+
@property(nonatomic, strong) NSTextField *nameLabel;
|
|
65
|
+
@property(nonatomic, strong) NSTextField *passwordLabel;
|
|
66
|
+
@property(nonatomic, strong) NSButton *accountSubmit;
|
|
67
|
+
@property(nonatomic, strong) NSButton *authModeButton;
|
|
68
|
+
@property(nonatomic, strong) NSButton *termsCheck;
|
|
69
|
+
@property(nonatomic, strong) NSTextField *termsVersion;
|
|
70
|
+
@property(nonatomic, strong) NSButton *termsSubmit;
|
|
71
|
+
@property(nonatomic, strong) NSButton *monthlyRadio;
|
|
72
|
+
@property(nonatomic, strong) NSButton *annualRadio;
|
|
73
|
+
@property(nonatomic, strong) NSButton *standardRadio;
|
|
74
|
+
@property(nonatomic, strong) NSButton *multiRadio;
|
|
75
|
+
@property(nonatomic, strong) NSTextField *standardPlan;
|
|
76
|
+
@property(nonatomic, strong) NSTextField *multiPlan;
|
|
52
77
|
@property(nonatomic, strong) NSTimer *timer;
|
|
78
|
+
@property(nonatomic, strong) NSFileHandle *setupInput;
|
|
53
79
|
@property(nonatomic, assign) pid_t pairProcessIdentifier;
|
|
54
80
|
@property(nonatomic, strong) dispatch_source_t pairProcessSource;
|
|
55
81
|
@property(nonatomic, copy) NSString *lastStage;
|
|
@@ -57,6 +83,7 @@ static BOOL acquireSetupInstanceLock(void) {
|
|
|
57
83
|
@property(nonatomic, copy) NSString *requiredUpdateURL;
|
|
58
84
|
@property(nonatomic, assign) BOOL setupStarted;
|
|
59
85
|
@property(nonatomic, assign) BOOL terminating;
|
|
86
|
+
@property(nonatomic, assign) BOOL signingIn;
|
|
60
87
|
@end
|
|
61
88
|
|
|
62
89
|
@implementation TrillboardsSetupDelegate
|
|
@@ -74,6 +101,24 @@ static BOOL acquireSetupInstanceLock(void) {
|
|
|
74
101
|
return field;
|
|
75
102
|
}
|
|
76
103
|
|
|
104
|
+
- (NSTextField *)inputWithFrame:(NSRect)frame placeholder:(NSString *)placeholder secure:(BOOL)secure {
|
|
105
|
+
NSTextField *field = secure
|
|
106
|
+
? [[NSSecureTextField alloc] initWithFrame:frame]
|
|
107
|
+
: [[NSTextField alloc] initWithFrame:frame];
|
|
108
|
+
field.placeholderString = placeholder;
|
|
109
|
+
field.font = [NSFont systemFontOfSize:14 weight:NSFontWeightRegular];
|
|
110
|
+
return field;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
- (NSButton *)panelButton:(NSString *)title frame:(NSRect)frame action:(SEL)action {
|
|
114
|
+
NSButton *button = [[NSButton alloc] initWithFrame:frame];
|
|
115
|
+
button.title = title;
|
|
116
|
+
button.bezelStyle = NSBezelStyleRounded;
|
|
117
|
+
button.target = self;
|
|
118
|
+
button.action = action;
|
|
119
|
+
return button;
|
|
120
|
+
}
|
|
121
|
+
|
|
77
122
|
- (void)applicationDidFinishLaunching:(NSNotification *)notification {
|
|
78
123
|
(void)notification;
|
|
79
124
|
self.lastStage = @"welcome";
|
|
@@ -118,6 +163,10 @@ static BOOL acquireSetupInstanceLock(void) {
|
|
|
118
163
|
- (void)stopPairProcess {
|
|
119
164
|
pid_t pid = self.pairProcessIdentifier;
|
|
120
165
|
self.pairProcessIdentifier = 0;
|
|
166
|
+
if (self.setupInput) {
|
|
167
|
+
[self.setupInput closeFile];
|
|
168
|
+
self.setupInput = nil;
|
|
169
|
+
}
|
|
121
170
|
if (self.pairProcessSource) {
|
|
122
171
|
dispatch_source_cancel(self.pairProcessSource);
|
|
123
172
|
self.pairProcessSource = nil;
|
|
@@ -150,12 +199,22 @@ static BOOL acquireSetupInstanceLock(void) {
|
|
|
150
199
|
return NO;
|
|
151
200
|
}
|
|
152
201
|
|
|
202
|
+
int inputPipe[2];
|
|
203
|
+
if (pipe(inputPipe) != 0) {
|
|
204
|
+
close(logFd);
|
|
205
|
+
if (error) *error = [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:nil];
|
|
206
|
+
return NO;
|
|
207
|
+
}
|
|
208
|
+
|
|
153
209
|
posix_spawn_file_actions_t fileActions;
|
|
154
210
|
posix_spawnattr_t attributes;
|
|
155
211
|
posix_spawn_file_actions_init(&fileActions);
|
|
156
212
|
posix_spawn_file_actions_adddup2(&fileActions, logFd, STDOUT_FILENO);
|
|
157
213
|
posix_spawn_file_actions_adddup2(&fileActions, logFd, STDERR_FILENO);
|
|
214
|
+
posix_spawn_file_actions_adddup2(&fileActions, inputPipe[0], STDIN_FILENO);
|
|
158
215
|
posix_spawn_file_actions_addclose(&fileActions, logFd);
|
|
216
|
+
posix_spawn_file_actions_addclose(&fileActions, inputPipe[0]);
|
|
217
|
+
posix_spawn_file_actions_addclose(&fileActions, inputPipe[1]);
|
|
159
218
|
posix_spawnattr_init(&attributes);
|
|
160
219
|
posix_spawnattr_setflags(&attributes, POSIX_SPAWN_SETPGROUP);
|
|
161
220
|
posix_spawnattr_setpgroup(&attributes, 0);
|
|
@@ -179,11 +238,14 @@ static BOOL acquireSetupInstanceLock(void) {
|
|
|
179
238
|
posix_spawnattr_destroy(&attributes);
|
|
180
239
|
posix_spawn_file_actions_destroy(&fileActions);
|
|
181
240
|
close(logFd);
|
|
241
|
+
close(inputPipe[0]);
|
|
182
242
|
if (spawnError != 0) {
|
|
243
|
+
close(inputPipe[1]);
|
|
183
244
|
if (error) *error = [NSError errorWithDomain:NSPOSIXErrorDomain code:spawnError userInfo:nil];
|
|
184
245
|
return NO;
|
|
185
246
|
}
|
|
186
247
|
|
|
248
|
+
self.setupInput = [[NSFileHandle alloc] initWithFileDescriptor:inputPipe[1] closeOnDealloc:YES];
|
|
187
249
|
self.pairProcessIdentifier = pid;
|
|
188
250
|
dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_PROC,
|
|
189
251
|
(uintptr_t)pid,
|
|
@@ -203,6 +265,10 @@ static BOOL acquireSetupInstanceLock(void) {
|
|
|
203
265
|
int status = 0;
|
|
204
266
|
pid_t waited = waitpid(pid, &status, 0);
|
|
205
267
|
self.pairProcessIdentifier = 0;
|
|
268
|
+
if (self.setupInput) {
|
|
269
|
+
[self.setupInput closeFile];
|
|
270
|
+
self.setupInput = nil;
|
|
271
|
+
}
|
|
206
272
|
if (self.pairProcessSource) {
|
|
207
273
|
dispatch_source_cancel(self.pairProcessSource);
|
|
208
274
|
self.pairProcessSource = nil;
|
|
@@ -241,7 +307,7 @@ static BOOL acquireSetupInstanceLock(void) {
|
|
|
241
307
|
NSTextField *brand = [self label:@"Sense Anything" size:16 weight:NSFontWeightSemibold frame:NSMakeRect(24, 480, 145, 28)];
|
|
242
308
|
[sidebar addSubview:brand];
|
|
243
309
|
|
|
244
|
-
NSArray<NSString *> *names = @[@"Welcome", @"Account", @"Recorder", @"Cameras", @"Local AI", @"Ready"];
|
|
310
|
+
NSArray<NSString *> *names = @[@"Welcome", @"Account", @"Account setup", @"Plan & Payment", @"Recorder", @"Cameras", @"Local AI", @"Ready"];
|
|
245
311
|
NSMutableArray<NSTextField *> *labels = [NSMutableArray array];
|
|
246
312
|
for (NSUInteger index = 0; index < names.count; index++) {
|
|
247
313
|
NSTextField *step = [self label:[NSString stringWithFormat:@"%lu %@", index + 1, names[index]]
|
|
@@ -274,6 +340,14 @@ static BOOL acquireSetupInstanceLock(void) {
|
|
|
274
340
|
self.statusValue.layer.cornerRadius = 6;
|
|
275
341
|
[root addSubview:self.statusValue];
|
|
276
342
|
|
|
343
|
+
self.googleReopenButton = [[NSButton alloc] initWithFrame:NSMakeRect(360, 219, 218, 28)];
|
|
344
|
+
self.googleReopenButton.title = @"Open sign-in again";
|
|
345
|
+
self.googleReopenButton.bezelStyle = NSBezelStyleRounded;
|
|
346
|
+
self.googleReopenButton.target = self;
|
|
347
|
+
self.googleReopenButton.action = @selector(googleReopenPressed);
|
|
348
|
+
self.googleReopenButton.hidden = YES;
|
|
349
|
+
[root addSubview:self.googleReopenButton];
|
|
350
|
+
|
|
277
351
|
self.metric = [self label:@"" size:13 weight:NSFontWeightRegular frame:NSMakeRect(236, 174, 465, 28)];
|
|
278
352
|
self.metric.textColor = NSColor.secondaryLabelColor;
|
|
279
353
|
[root addSubview:self.metric];
|
|
@@ -300,12 +374,12 @@ static BOOL acquireSetupInstanceLock(void) {
|
|
|
300
374
|
self.updateStatus.textColor = NSColor.secondaryLabelColor;
|
|
301
375
|
[root addSubview:self.updateStatus];
|
|
302
376
|
|
|
303
|
-
|
|
377
|
+
self.privacyLabel = [self label:@"Camera credentials stay on this computer. Cloud inference is off by default."
|
|
304
378
|
size:12
|
|
305
379
|
weight:NSFontWeightRegular
|
|
306
380
|
frame:NSMakeRect(236, 34, 300, 42)];
|
|
307
|
-
|
|
308
|
-
[root addSubview:
|
|
381
|
+
self.privacyLabel.textColor = NSColor.secondaryLabelColor;
|
|
382
|
+
[root addSubview:self.privacyLabel];
|
|
309
383
|
|
|
310
384
|
self.actionButton = [[NSButton alloc] initWithFrame:NSMakeRect(562, 36, 139, 40)];
|
|
311
385
|
self.actionButton.title = @"Start setup";
|
|
@@ -321,9 +395,234 @@ static BOOL acquireSetupInstanceLock(void) {
|
|
|
321
395
|
self.closeButton.action = @selector(closePressed);
|
|
322
396
|
self.closeButton.hidden = YES;
|
|
323
397
|
[root addSubview:self.closeButton];
|
|
398
|
+
|
|
399
|
+
// Native onboarding panels. Provider-owned Google and Stripe UI opens in
|
|
400
|
+
// the system browser; every product-owned choice remains in this window.
|
|
401
|
+
self.accountPanel = [[NSView alloc] initWithFrame:NSMakeRect(236, 78, 465, 220)];
|
|
402
|
+
NSButton *google = [self panelButton:@"Continue with Google"
|
|
403
|
+
frame:NSMakeRect(0, 178, 465, 34)
|
|
404
|
+
action:@selector(googlePressed)];
|
|
405
|
+
[self.accountPanel addSubview:google];
|
|
406
|
+
NSTextField *orEmail = [self label:@"or use email" size:11 weight:NSFontWeightRegular frame:NSMakeRect(0, 153, 465, 18)];
|
|
407
|
+
orEmail.alignment = NSTextAlignmentCenter;
|
|
408
|
+
orEmail.textColor = NSColor.secondaryLabelColor;
|
|
409
|
+
[self.accountPanel addSubview:orEmail];
|
|
410
|
+
self.nameLabel = [self label:@"Full name" size:11 weight:NSFontWeightRegular frame:NSMakeRect(0, 132, 220, 17)];
|
|
411
|
+
[self.accountPanel addSubview:self.nameLabel];
|
|
412
|
+
self.nameInput = [self inputWithFrame:NSMakeRect(0, 103, 220, 28) placeholder:@"Maya Chen" secure:NO];
|
|
413
|
+
[self.accountPanel addSubview:self.nameInput];
|
|
414
|
+
NSTextField *emailLabel = [self label:@"Work email" size:11 weight:NSFontWeightRegular frame:NSMakeRect(235, 132, 230, 17)];
|
|
415
|
+
[self.accountPanel addSubview:emailLabel];
|
|
416
|
+
self.emailInput = [self inputWithFrame:NSMakeRect(235, 103, 230, 28) placeholder:@"maya@yourshop.com" secure:NO];
|
|
417
|
+
[self.accountPanel addSubview:self.emailInput];
|
|
418
|
+
self.passwordLabel = [self label:@"Create password" size:11 weight:NSFontWeightRegular frame:NSMakeRect(0, 78, 220, 17)];
|
|
419
|
+
[self.accountPanel addSubview:self.passwordLabel];
|
|
420
|
+
self.passwordInput = (NSSecureTextField *)[self inputWithFrame:NSMakeRect(0, 49, 220, 28) placeholder:@"At least 8 characters" secure:YES];
|
|
421
|
+
[self.accountPanel addSubview:self.passwordInput];
|
|
422
|
+
self.authModeButton = [self panelButton:@"Already have an account? Sign in"
|
|
423
|
+
frame:NSMakeRect(230, 49, 235, 28)
|
|
424
|
+
action:@selector(authModePressed)];
|
|
425
|
+
[self.accountPanel addSubview:self.authModeButton];
|
|
426
|
+
self.accountSubmit = [self panelButton:@"Create account"
|
|
427
|
+
frame:NSMakeRect(285, 5, 180, 34)
|
|
428
|
+
action:@selector(accountSubmitPressed)];
|
|
429
|
+
[self.accountPanel addSubview:self.accountSubmit];
|
|
430
|
+
[root addSubview:self.accountPanel];
|
|
431
|
+
|
|
432
|
+
self.termsPanel = [[NSView alloc] initWithFrame:NSMakeRect(236, 78, 465, 220)];
|
|
433
|
+
NSTextField *termsSummary = [self label:@"Review and accept the current Sense Anything terms, privacy notice, earner agreement, and data processing addendum for this account."
|
|
434
|
+
size:13 weight:NSFontWeightRegular frame:NSMakeRect(0, 150, 465, 58)];
|
|
435
|
+
termsSummary.textColor = NSColor.secondaryLabelColor;
|
|
436
|
+
[self.termsPanel addSubview:termsSummary];
|
|
437
|
+
self.termsCheck = [NSButton checkboxWithTitle:@"I reviewed and accept the current agreements shown for this account."
|
|
438
|
+
target:self
|
|
439
|
+
action:@selector(termsCheckPressed)];
|
|
440
|
+
self.termsCheck.frame = NSMakeRect(0, 103, 465, 38);
|
|
441
|
+
[self.termsPanel addSubview:self.termsCheck];
|
|
442
|
+
self.termsVersion = [self label:@"" size:11 weight:NSFontWeightRegular frame:NSMakeRect(24, 66, 430, 34)];
|
|
443
|
+
self.termsVersion.textColor = NSColor.secondaryLabelColor;
|
|
444
|
+
[self.termsPanel addSubview:self.termsVersion];
|
|
445
|
+
NSButton *reviewTerms = [self panelButton:@"Review terms"
|
|
446
|
+
frame:NSMakeRect(0, 39, 145, 24)
|
|
447
|
+
action:@selector(reviewTermsPressed)];
|
|
448
|
+
[self.termsPanel addSubview:reviewTerms];
|
|
449
|
+
NSButton *reviewPrivacy = [self panelButton:@"Review privacy"
|
|
450
|
+
frame:NSMakeRect(150, 39, 145, 24)
|
|
451
|
+
action:@selector(reviewPrivacyPressed)];
|
|
452
|
+
[self.termsPanel addSubview:reviewPrivacy];
|
|
453
|
+
NSButton *reviewDpa = [self panelButton:@"Review DPA"
|
|
454
|
+
frame:NSMakeRect(300, 39, 145, 24)
|
|
455
|
+
action:@selector(reviewDpaPressed)];
|
|
456
|
+
[self.termsPanel addSubview:reviewDpa];
|
|
457
|
+
self.termsSubmit = [self panelButton:@"Accept & continue"
|
|
458
|
+
frame:NSMakeRect(285, 5, 180, 34)
|
|
459
|
+
action:@selector(termsSubmitPressed)];
|
|
460
|
+
self.termsSubmit.enabled = NO;
|
|
461
|
+
[self.termsPanel addSubview:self.termsSubmit];
|
|
462
|
+
[root addSubview:self.termsPanel];
|
|
463
|
+
|
|
464
|
+
self.planPanel = [[NSView alloc] initWithFrame:NSMakeRect(236, 78, 465, 220)];
|
|
465
|
+
self.monthlyRadio = [NSButton radioButtonWithTitle:@"Monthly" target:self action:@selector(intervalPressed:)];
|
|
466
|
+
self.monthlyRadio.frame = NSMakeRect(0, 181, 100, 24);
|
|
467
|
+
self.monthlyRadio.state = NSControlStateValueOn;
|
|
468
|
+
[self.planPanel addSubview:self.monthlyRadio];
|
|
469
|
+
self.annualRadio = [NSButton radioButtonWithTitle:@"Annual" target:self action:@selector(intervalPressed:)];
|
|
470
|
+
self.annualRadio.frame = NSMakeRect(105, 181, 100, 24);
|
|
471
|
+
[self.planPanel addSubview:self.annualRadio];
|
|
472
|
+
self.standardRadio = [NSButton radioButtonWithTitle:@"" target:self action:@selector(planPressed:)];
|
|
473
|
+
self.standardRadio.frame = NSMakeRect(0, 120, 22, 24);
|
|
474
|
+
self.standardRadio.state = NSControlStateValueOn;
|
|
475
|
+
[self.planPanel addSubview:self.standardRadio];
|
|
476
|
+
self.standardPlan = [self label:@"Sense Anything" size:14 weight:NSFontWeightSemibold frame:NSMakeRect(28, 105, 195, 58)];
|
|
477
|
+
[self.planPanel addSubview:self.standardPlan];
|
|
478
|
+
self.multiRadio = [NSButton radioButtonWithTitle:@"" target:self action:@selector(planPressed:)];
|
|
479
|
+
self.multiRadio.frame = NSMakeRect(238, 120, 22, 24);
|
|
480
|
+
[self.planPanel addSubview:self.multiRadio];
|
|
481
|
+
self.multiPlan = [self label:@"Multi-site" size:14 weight:NSFontWeightSemibold frame:NSMakeRect(266, 105, 199, 58)];
|
|
482
|
+
[self.planPanel addSubview:self.multiPlan];
|
|
483
|
+
NSTextField *planNote = [self label:@"Stripe handles checkout securely in your browser. This installer resumes automatically when your plan is active."
|
|
484
|
+
size:11 weight:NSFontWeightRegular frame:NSMakeRect(0, 52, 465, 42)];
|
|
485
|
+
planNote.textColor = NSColor.secondaryLabelColor;
|
|
486
|
+
[self.planPanel addSubview:planNote];
|
|
487
|
+
NSButton *planSubmit = [self panelButton:@"Continue to checkout"
|
|
488
|
+
frame:NSMakeRect(265, 5, 200, 34)
|
|
489
|
+
action:@selector(planSubmitPressed)];
|
|
490
|
+
[self.planPanel addSubview:planSubmit];
|
|
491
|
+
[root addSubview:self.planPanel];
|
|
492
|
+
|
|
493
|
+
self.chooseAccountPanel = [[NSView alloc] initWithFrame:NSMakeRect(236, 78, 465, 220)];
|
|
494
|
+
self.accountPicker = [[NSPopUpButton alloc] initWithFrame:NSMakeRect(0, 135, 465, 32) pullsDown:NO];
|
|
495
|
+
[self.chooseAccountPanel addSubview:self.accountPicker];
|
|
496
|
+
self.chooseAccountSubmit = [self panelButton:@"Continue with this account" frame:NSMakeRect(215, 0, 250, 34) action:@selector(chooseAccountPressed)];
|
|
497
|
+
[self.chooseAccountPanel addSubview:self.chooseAccountSubmit];
|
|
498
|
+
[root addSubview:self.chooseAccountPanel];
|
|
499
|
+
|
|
500
|
+
self.onboardingPanels = @[self.accountPanel, self.termsPanel, self.planPanel, self.chooseAccountPanel];
|
|
501
|
+
[self showOnboardingPanel:nil];
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
- (void)showOnboardingPanel:(NSView *)panel {
|
|
505
|
+
for (NSView *candidate in self.onboardingPanels ?: @[]) candidate.hidden = candidate != panel;
|
|
506
|
+
BOOL showStatus = panel == nil;
|
|
507
|
+
self.statusValue.hidden = !showStatus;
|
|
508
|
+
self.metric.hidden = !showStatus;
|
|
509
|
+
self.progress.hidden = !showStatus;
|
|
510
|
+
self.updateButton.hidden = !showStatus;
|
|
511
|
+
self.updateStatus.hidden = !showStatus;
|
|
512
|
+
self.privacyLabel.hidden = !showStatus;
|
|
513
|
+
self.actionButton.hidden = !showStatus;
|
|
514
|
+
self.closeButton.hidden = YES;
|
|
515
|
+
self.googleReopenButton.hidden = YES;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
- (void)chooseAccountPressed {
|
|
519
|
+
NSString *owner = self.accountPicker.selectedItem.representedObject;
|
|
520
|
+
if (!owner.length) return;
|
|
521
|
+
self.chooseAccountSubmit.enabled = NO;
|
|
522
|
+
[self sendOnboardingCommand:@{ @"action": @"select_account", @"ownerUserId": owner }];
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
- (void)sendOnboardingCommand:(NSDictionary *)command {
|
|
526
|
+
if (!self.setupInput || ![NSJSONSerialization isValidJSONObject:command]) return;
|
|
527
|
+
NSData *json = [NSJSONSerialization dataWithJSONObject:command options:0 error:nil];
|
|
528
|
+
if (!json) return;
|
|
529
|
+
NSMutableData *line = [json mutableCopy];
|
|
530
|
+
[line appendData:[@"\n" dataUsingEncoding:NSUTF8StringEncoding]];
|
|
531
|
+
@try {
|
|
532
|
+
[self.setupInput writeData:line];
|
|
533
|
+
} @catch (__unused NSException *exception) {
|
|
534
|
+
[self showLocalError:@"Could not continue setup"
|
|
535
|
+
message:@"Close and reopen Sense Anything Connector, then try again."];
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
- (void)googleReopenPressed {
|
|
540
|
+
[self sendOnboardingCommand:@{ @"action": @"google_reopen" }];
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
- (void)googlePressed {
|
|
544
|
+
[self sendOnboardingCommand:@{ @"action": @"google_start" }];
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
- (void)authModePressed {
|
|
548
|
+
self.signingIn = !self.signingIn;
|
|
549
|
+
self.nameLabel.hidden = self.signingIn;
|
|
550
|
+
self.nameInput.hidden = self.signingIn;
|
|
551
|
+
self.passwordLabel.stringValue = self.signingIn ? @"Password" : @"Create password";
|
|
552
|
+
self.passwordInput.placeholderString = self.signingIn ? @"Your password" : @"At least 8 characters";
|
|
553
|
+
self.accountSubmit.title = self.signingIn ? @"Sign in & continue" : @"Create account";
|
|
554
|
+
self.authModeButton.title = self.signingIn ? @"New here? Create account" : @"Already have an account? Sign in";
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
- (void)showInputMessage:(NSString *)message {
|
|
558
|
+
NSAlert *alert = [[NSAlert alloc] init];
|
|
559
|
+
alert.messageText = @"Sense Anything";
|
|
560
|
+
alert.informativeText = message;
|
|
561
|
+
[alert addButtonWithTitle:@"OK"];
|
|
562
|
+
[alert runModal];
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
- (void)accountSubmitPressed {
|
|
566
|
+
NSString *email = [self.emailInput.stringValue stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceAndNewlineCharacterSet];
|
|
567
|
+
NSString *password = self.passwordInput.stringValue;
|
|
568
|
+
NSString *name = [self.nameInput.stringValue stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceAndNewlineCharacterSet];
|
|
569
|
+
if (email.length == 0 || password.length == 0) {
|
|
570
|
+
[self showInputMessage:@"Enter your email and password to continue."];
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
if (!self.signingIn && (name.length == 0 || password.length < 8)) {
|
|
574
|
+
[self showInputMessage:@"Enter your name and a password with at least 8 characters."];
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
NSDictionary *command = self.signingIn
|
|
578
|
+
? @{ @"action": @"email_sign_in", @"email": email, @"password": password }
|
|
579
|
+
: @{ @"action": @"email_sign_up", @"name": name, @"email": email, @"password": password };
|
|
580
|
+
[self sendOnboardingCommand:command];
|
|
581
|
+
self.passwordInput.stringValue = @"";
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
- (void)termsCheckPressed {
|
|
585
|
+
self.termsSubmit.enabled = self.termsCheck.state == NSControlStateValueOn;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
- (void)reviewTermsPressed {
|
|
589
|
+
[self openURL:@"https://trillboards.com/legal/terms"];
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
- (void)reviewPrivacyPressed {
|
|
593
|
+
[self openURL:@"https://trillboards.com/legal/privacy-policy"];
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
- (void)reviewDpaPressed {
|
|
597
|
+
[self openURL:@"https://trillboards.com/legal/data-processing"];
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
- (void)termsSubmitPressed {
|
|
601
|
+
if (self.termsCheck.state != NSControlStateValueOn) return;
|
|
602
|
+
self.termsSubmit.enabled = NO;
|
|
603
|
+
[self sendOnboardingCommand:@{ @"action": @"accept_terms" }];
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
- (void)planSubmitPressed {
|
|
607
|
+
NSButton *selected = self.multiRadio.state == NSControlStateValueOn ? self.multiRadio : self.standardRadio;
|
|
608
|
+
NSString *tier = selected.identifier;
|
|
609
|
+
if (tier.length == 0) return;
|
|
610
|
+
NSString *interval = self.annualRadio.state == NSControlStateValueOn ? @"annual" : @"monthly";
|
|
611
|
+
[self sendOnboardingCommand:@{ @"action": @"select_plan", @"tier": tier, @"interval": interval }];
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
- (void)intervalPressed:(NSButton *)sender {
|
|
615
|
+
self.monthlyRadio.state = sender == self.monthlyRadio ? NSControlStateValueOn : NSControlStateValueOff;
|
|
616
|
+
self.annualRadio.state = sender == self.annualRadio ? NSControlStateValueOn : NSControlStateValueOff;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
- (void)planPressed:(NSButton *)sender {
|
|
620
|
+
self.standardRadio.state = sender == self.standardRadio ? NSControlStateValueOn : NSControlStateValueOff;
|
|
621
|
+
self.multiRadio.state = sender == self.multiRadio ? NSControlStateValueOn : NSControlStateValueOff;
|
|
324
622
|
}
|
|
325
623
|
|
|
326
624
|
- (void)showWelcome {
|
|
625
|
+
[self showOnboardingPanel:nil];
|
|
327
626
|
self.lastStage = @"welcome";
|
|
328
627
|
self.headline.stringValue = @"Set up this computer for CCTV";
|
|
329
628
|
self.detail.stringValue = @"Connect your Sense Anything account, find your recorder, and verify every camera before screens are created.";
|
|
@@ -338,6 +637,7 @@ static BOOL acquireSetupInstanceLock(void) {
|
|
|
338
637
|
}
|
|
339
638
|
|
|
340
639
|
- (void)showLocalError:(NSString *)title message:(NSString *)message {
|
|
640
|
+
[self showOnboardingPanel:nil];
|
|
341
641
|
self.lastStage = @"error";
|
|
342
642
|
self.headline.stringValue = title;
|
|
343
643
|
self.detail.stringValue = message;
|
|
@@ -348,6 +648,7 @@ static BOOL acquireSetupInstanceLock(void) {
|
|
|
348
648
|
self.actionButton.title = @"Try again";
|
|
349
649
|
self.actionButton.enabled = YES;
|
|
350
650
|
self.closeButton.hidden = YES;
|
|
651
|
+
self.googleReopenButton.hidden = YES;
|
|
351
652
|
}
|
|
352
653
|
|
|
353
654
|
- (void)closePressed {
|
|
@@ -450,7 +751,10 @@ static BOOL acquireSetupInstanceLock(void) {
|
|
|
450
751
|
[self openURL:self.requiredUpdateURL];
|
|
451
752
|
return;
|
|
452
753
|
}
|
|
453
|
-
if ([self.lastStage isEqualToString:@"
|
|
754
|
+
if ([self.lastStage isEqualToString:@"onboarding_google"] ||
|
|
755
|
+
[self.lastStage isEqualToString:@"onboarding_checkout"]) {
|
|
756
|
+
[self sendOnboardingCommand:@{ @"action": @"refresh" }];
|
|
757
|
+
} else if ([self.lastStage isEqualToString:@"waiting_for_account"] ||
|
|
454
758
|
[self.lastStage isEqualToString:@"account_attention"]) {
|
|
455
759
|
[self openURL:self.portalURL];
|
|
456
760
|
} else if ([self.lastStage isEqualToString:@"ready"]) {
|
|
@@ -510,6 +814,7 @@ static BOOL acquireSetupInstanceLock(void) {
|
|
|
510
814
|
- (void)renderState:(NSDictionary *)state {
|
|
511
815
|
NSString *stage = [state[@"stage"] isKindOfClass:NSString.class] ? state[@"stage"] : nil;
|
|
512
816
|
if (!stage) return;
|
|
817
|
+
NSString *previousStage = self.lastStage;
|
|
513
818
|
self.lastStage = stage;
|
|
514
819
|
if ([state[@"portalUrl"] isKindOfClass:NSString.class]) self.portalURL = state[@"portalUrl"];
|
|
515
820
|
if ([state[@"title"] isKindOfClass:NSString.class]) self.headline.stringValue = state[@"title"];
|
|
@@ -519,11 +824,86 @@ static BOOL acquireSetupInstanceLock(void) {
|
|
|
519
824
|
NSNumber *validated = state[@"validatedFeeds"];
|
|
520
825
|
NSNumber *devices = state[@"devicesFound"];
|
|
521
826
|
NSNumber *rejected = state[@"rejectedFeeds"];
|
|
827
|
+
if ([stage isEqualToString:@"onboarding_account"]) {
|
|
828
|
+
[self showOnboardingPanel:self.accountPanel];
|
|
829
|
+
[self updateSteps:1];
|
|
830
|
+
self.accountSubmit.enabled = YES;
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
833
|
+
if ([stage isEqualToString:@"onboarding_choose_account"]) {
|
|
834
|
+
[self showOnboardingPanel:self.chooseAccountPanel];
|
|
835
|
+
[self updateSteps:2];
|
|
836
|
+
[self.accountPicker removeAllItems];
|
|
837
|
+
NSArray *accounts = [state[@"accounts"] isKindOfClass:NSArray.class] ? state[@"accounts"] : @[];
|
|
838
|
+
for (NSDictionary *account in accounts) {
|
|
839
|
+
NSString *label = [NSString stringWithFormat:@"%@ (%@)", account[@"label"] ?: @"Account", account[@"role"] ?: @""];
|
|
840
|
+
NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:label action:nil keyEquivalent:@""];
|
|
841
|
+
item.representedObject = account[@"ownerUserId"];
|
|
842
|
+
[self.accountPicker.menu addItem:item];
|
|
843
|
+
}
|
|
844
|
+
if (accounts.count) [self.accountPicker selectItemAtIndex:0];
|
|
845
|
+
self.chooseAccountSubmit.enabled = accounts.count > 0;
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
848
|
+
if ([stage isEqualToString:@"onboarding_terms"]) {
|
|
849
|
+
[self showOnboardingPanel:self.termsPanel];
|
|
850
|
+
[self updateSteps:2];
|
|
851
|
+
if (![previousStage isEqualToString:@"onboarding_terms"]) {
|
|
852
|
+
self.termsCheck.state = NSControlStateValueOff;
|
|
853
|
+
self.termsSubmit.enabled = NO;
|
|
854
|
+
}
|
|
855
|
+
self.termsSubmit.enabled = self.termsCheck.state == NSControlStateValueOn;
|
|
856
|
+
NSArray *agreements = [state[@"requiredAgreements"] isKindOfClass:NSArray.class] ? state[@"requiredAgreements"] : @[];
|
|
857
|
+
NSMutableArray<NSString *> *versions = [NSMutableArray array];
|
|
858
|
+
for (NSDictionary *agreement in agreements) {
|
|
859
|
+
if (![agreement isKindOfClass:NSDictionary.class]) continue;
|
|
860
|
+
NSString *type = [agreement[@"type"] isKindOfClass:NSString.class] ? agreement[@"type"] : @"";
|
|
861
|
+
NSString *label = [agreement[@"label"] isKindOfClass:NSString.class] ? agreement[@"label"] : [type stringByReplacingOccurrencesOfString:@"_" withString:@" "];
|
|
862
|
+
NSString *version = [agreement[@"version"] isKindOfClass:NSString.class] ? agreement[@"version"] : @"";
|
|
863
|
+
if (type.length && version.length) [versions addObject:[NSString stringWithFormat:@"%@ v%@", label, version]];
|
|
864
|
+
}
|
|
865
|
+
self.termsVersion.stringValue = [versions componentsJoinedByString:@" • "];
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
868
|
+
if ([stage isEqualToString:@"onboarding_plan"]) {
|
|
869
|
+
[self showOnboardingPanel:self.planPanel];
|
|
870
|
+
[self updateSteps:3];
|
|
871
|
+
NSArray *plans = [state[@"plans"] isKindOfClass:NSArray.class] ? state[@"plans"] : @[];
|
|
872
|
+
if (plans.count > 0) {
|
|
873
|
+
NSDictionary *first = plans[0];
|
|
874
|
+
self.standardRadio.identifier = [first[@"tier"] isKindOfClass:NSString.class] ? first[@"tier"] : @"";
|
|
875
|
+
self.standardPlan.stringValue = [NSString stringWithFormat:@"%@\n%@/month • %@/year",
|
|
876
|
+
first[@"label"] ?: @"Sense Anything", first[@"monthly"] ?: @"—", first[@"annual"] ?: @"—"];
|
|
877
|
+
self.standardRadio.hidden = NO;
|
|
878
|
+
self.standardPlan.hidden = NO;
|
|
879
|
+
}
|
|
880
|
+
if (plans.count > 1) {
|
|
881
|
+
NSDictionary *second = plans[1];
|
|
882
|
+
self.multiRadio.identifier = [second[@"tier"] isKindOfClass:NSString.class] ? second[@"tier"] : @"";
|
|
883
|
+
self.multiPlan.stringValue = [NSString stringWithFormat:@"%@\n%@/month • %@/year",
|
|
884
|
+
second[@"label"] ?: @"Multi-site", second[@"monthly"] ?: @"—", second[@"annual"] ?: @"—"];
|
|
885
|
+
self.multiRadio.hidden = NO;
|
|
886
|
+
self.multiPlan.hidden = NO;
|
|
887
|
+
} else {
|
|
888
|
+
self.multiRadio.hidden = YES;
|
|
889
|
+
self.multiPlan.hidden = YES;
|
|
890
|
+
}
|
|
891
|
+
return;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
[self showOnboardingPanel:nil];
|
|
522
895
|
if (([stage isEqualToString:@"waiting_for_account"] ||
|
|
523
896
|
[stage isEqualToString:@"account_attention"]) &&
|
|
524
897
|
[state[@"displayCode"] isKindOfClass:NSString.class]) {
|
|
525
898
|
self.statusValue.font = [NSFont monospacedSystemFontOfSize:25 weight:NSFontWeightSemibold];
|
|
526
899
|
self.statusValue.stringValue = state[@"displayCode"];
|
|
900
|
+
} else if ([stage isEqualToString:@"onboarding_google"]) {
|
|
901
|
+
self.statusValue.stringValue = @"Waiting for Google";
|
|
902
|
+
self.googleReopenButton.hidden = NO;
|
|
903
|
+
} else if ([stage isEqualToString:@"onboarding_checkout"]) {
|
|
904
|
+
self.statusValue.stringValue = @"Waiting for Stripe";
|
|
905
|
+
} else if ([stage isEqualToString:@"onboarding_install"]) {
|
|
906
|
+
self.statusValue.stringValue = @"Onboarding complete";
|
|
527
907
|
} else if (cameraCount.integerValue > 0) {
|
|
528
908
|
self.statusValue.font = [NSFont systemFontOfSize:25 weight:NSFontWeightSemibold];
|
|
529
909
|
self.statusValue.stringValue = [NSString stringWithFormat:@"%ld camera%@", cameraCount.integerValue, cameraCount.integerValue == 1 ? @"" : @"s"];
|
|
@@ -566,7 +946,16 @@ static BOOL acquireSetupInstanceLock(void) {
|
|
|
566
946
|
self.progress.doubleValue = [self progressForStage:stage];
|
|
567
947
|
[self updateSteps:[self stepForStage:stage]];
|
|
568
948
|
self.closeButton.hidden = ![stage isEqualToString:@"ready"];
|
|
569
|
-
if ([stage isEqualToString:@"
|
|
949
|
+
if ([stage isEqualToString:@"onboarding_google"]) {
|
|
950
|
+
self.actionButton.title = @"Use another sign-in";
|
|
951
|
+
self.actionButton.enabled = YES;
|
|
952
|
+
} else if ([stage isEqualToString:@"onboarding_checkout"]) {
|
|
953
|
+
self.actionButton.title = @"Back to plans";
|
|
954
|
+
self.actionButton.enabled = YES;
|
|
955
|
+
} else if ([stage isEqualToString:@"onboarding_install"]) {
|
|
956
|
+
self.actionButton.title = @"Preparing cameras…";
|
|
957
|
+
self.actionButton.enabled = NO;
|
|
958
|
+
} else if ([stage isEqualToString:@"waiting_for_account"]) {
|
|
570
959
|
self.actionButton.title = @"Open Sense Anything";
|
|
571
960
|
self.actionButton.enabled = YES;
|
|
572
961
|
} else if ([stage isEqualToString:@"account_attention"]) {
|
|
@@ -592,25 +981,29 @@ static BOOL acquireSetupInstanceLock(void) {
|
|
|
592
981
|
}
|
|
593
982
|
|
|
594
983
|
- (NSInteger)stepForStage:(NSString *)stage {
|
|
595
|
-
if ([@[@"
|
|
596
|
-
if ([@[@"
|
|
597
|
-
if ([@[@"
|
|
598
|
-
if ([@[@"
|
|
599
|
-
if ([
|
|
984
|
+
if ([@[@"onboarding_account", @"onboarding_google"] containsObject:stage]) return 1;
|
|
985
|
+
if ([@[@"onboarding_terms", @"onboarding_choose_account"] containsObject:stage]) return 2;
|
|
986
|
+
if ([@[@"onboarding_plan", @"onboarding_checkout"] containsObject:stage]) return 3;
|
|
987
|
+
if ([@[@"onboarding_install", @"starting", @"waiting_for_account", @"account_attention", @"account_connected", @"discovering", @"devices_found"] containsObject:stage]) return 4;
|
|
988
|
+
if ([@[@"validating", @"cameras_created"] containsObject:stage]) return 5;
|
|
989
|
+
if ([@[@"local_ai", @"local_ai_error", @"starting_service"] containsObject:stage]) return 6;
|
|
990
|
+
if ([stage isEqualToString:@"ready"]) return 7;
|
|
600
991
|
return 0;
|
|
601
992
|
}
|
|
602
993
|
|
|
603
994
|
- (double)progressForStage:(NSString *)stage {
|
|
604
995
|
NSDictionary<NSString *, NSNumber *> *values = @{
|
|
605
|
-
@"
|
|
606
|
-
@"
|
|
996
|
+
@"onboarding_account": @10, @"onboarding_google": @12, @"onboarding_terms": @25,
|
|
997
|
+
@"onboarding_plan": @42, @"onboarding_checkout": @50, @"onboarding_install": @65,
|
|
998
|
+
@"waiting_for_account": @65, @"account_attention": @65, @"account_connected": @65, @"discovering": @68,
|
|
999
|
+
@"devices_found": @70, @"validating": @75, @"cameras_created": @80,
|
|
607
1000
|
@"local_ai": @88, @"starting_service": @94, @"ready": @100
|
|
608
1001
|
};
|
|
609
1002
|
return values[stage] ? values[stage].doubleValue : 8;
|
|
610
1003
|
}
|
|
611
1004
|
|
|
612
1005
|
- (void)updateSteps:(NSInteger)active {
|
|
613
|
-
NSArray<NSString *> *names = @[@"Welcome", @"Account", @"Recorder", @"Cameras", @"Local AI", @"Ready"];
|
|
1006
|
+
NSArray<NSString *> *names = @[@"Welcome", @"Account", @"Account setup", @"Plan & Payment", @"Recorder", @"Cameras", @"Local AI", @"Ready"];
|
|
614
1007
|
for (NSInteger index = 0; index < (NSInteger)self.stepLabels.count; index++) {
|
|
615
1008
|
NSTextField *field = self.stepLabels[(NSUInteger)index];
|
|
616
1009
|
NSString *prefix = index < active ? @"OK" : [NSString stringWithFormat:@"%ld", index + 1];
|
|
@@ -363,7 +363,12 @@ if [ "${SETUP_MODE:-0}" = "1" ]; then
|
|
|
363
363
|
plog "setup: starting validate-first setup mode"
|
|
364
364
|
set +e
|
|
365
365
|
if is_ui; then
|
|
366
|
-
|
|
366
|
+
# The native app owns account, legal and plan selection. This long-lived
|
|
367
|
+
# process receives commands on inherited stdin and exits only after the API
|
|
368
|
+
# says legal + entitlement are current, so cameras cannot open early.
|
|
369
|
+
# The same process keeps the authenticated session in memory through camera
|
|
370
|
+
# setup and mints a one-use signed-in browser handoff at first launch.
|
|
371
|
+
"$LAUNCHER" installer-onboarding --config "$CONFIG" --status-file "$UI_STATUS_FILE"
|
|
367
372
|
else
|
|
368
373
|
"$LAUNCHER" setup --config "$CONFIG"
|
|
369
374
|
fi
|
|
@@ -148,7 +148,13 @@ REM ── Validate-first setup: the CLI displays the code, keeps this window's
|
|
|
148
148
|
REM staged progress, and exits 0 once the portal finalizes cameras and the
|
|
149
149
|
REM config is written — then the SAME honest service stages below run.
|
|
150
150
|
if defined UI_STATUS_FILE (
|
|
151
|
-
|
|
151
|
+
REM Account, current legal versions and paid entitlement are completed in
|
|
152
|
+
REM the native installer BEFORE camera setup is allowed to open the portal.
|
|
153
|
+
REM Commands arrive on inherited stdin; passwords never touch argv or disk.
|
|
154
|
+
REM The onboarding process keeps its authenticated session in memory, starts
|
|
155
|
+
REM camera setup itself, and wraps the first portal launch in a one-use login
|
|
156
|
+
REM handoff. Email users therefore do not sign in a second time in the browser.
|
|
157
|
+
call "%~dp0trillboards-edge.cmd" installer-onboarding --config "%CFG%" --status-file "%UI_STATUS_FILE%"
|
|
152
158
|
) else (
|
|
153
159
|
call "%~dp0trillboards-edge.cmd" setup --config "%CFG%"
|
|
154
160
|
)
|