Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
45e3077
fix: finish referral and integration connection recovery
aryanorastar Oct 16, 2026
d096aa8
test: use a valid MCP detail in account-switch regression
aryanorastar Oct 16, 2026
a56f1f7
fix: ignore browser receipts after authorization owner changes
aryanorastar Oct 16, 2026
482a6c1
fix: retire pending authorization when task selection changes
aryanorastar Oct 16, 2026
55f85de
fix(app): let the latest task selection own its whole authorization
aryanorastar Oct 16, 2026
d9fa6f5
fix(app): tie MCP registration to its URL and serialize task default …
aryanorastar Oct 18, 2026
a22b2e3
fix(app): retire a whole MCP connect attempt when its URL is edited
aryanorastar Oct 19, 2026
5a002b7
Merge remote-tracking branch 'origin/main' into fix/connection-comple…
aryanorastar Oct 19, 2026
0f5dc4c
fix(app): release MCP polling for its own wait even after an account …
aryanorastar Oct 19, 2026
1bb10cc
Merge remote-tracking branch 'origin/main' into fix/connection-comple…
aryanorastar Oct 20, 2026
a762d47
Merge origin/main into connection recovery
aryanorastar Oct 23, 2026
2cc2f0e
test(app): align task-delivery fixtures with recovery reads
aryanorastar Oct 23, 2026
283c148
fix(app,web): finish non-OAuth MCP setup, fence registrations, keep r…
aryanorastar Oct 23, 2026
70b84d7
fix(app,backend): fence MCP retry cache, fresh OAuth on retry, align …
aryanorastar Oct 24, 2026
bf87146
Merge origin/main into fix/connection-completion-recovery
aryanorastar Oct 24, 2026
b98b032
chore(desktop): regenerate Swift API DTOs for the MCP authorize route
aryanorastar Oct 24, 2026
fbc7212
fix(app,backend): serialize MCP authorization retries; rotate only th…
aryanorastar Oct 25, 2026
09acc4d
Merge current main and preserve fresh-grant connection recovery
aryanorastar Oct 25, 2026
51cdfd3
test(app): migrate plugin setup security checks to typed verdicts
aryanorastar Oct 25, 2026
ed6cd0f
fix(auth): fence MCP callback commits against newer retries
aryanorastar Oct 25, 2026
a6c4d7b
test(backend): isolate app visibility guards from module stubs
aryanorastar Oct 25, 2026
c846bbb
docs(auth): separate recovery readiness from Firebase helper rollout
aryanorastar Oct 25, 2026
223d4c6
ci: refresh security analysis after GitHub API timeout
aryanorastar Oct 25, 2026
918e2b6
test(auth): constrain local OAuth redirect headers
aryanorastar Oct 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"version": 1,
"images": [
{
"file": "001-web-referral-retry.png",
"platform": "web",
"description": "Real LoginClient with synthetic auth and local failing/successful claim responses; no production account changed.",
"captured_by": "agent",
"source": "screencapture"
},
{
"file": "002-web-referral-retried.png",
"platform": "web",
"description": "Real LoginClient with synthetic auth and local failing/successful claim responses; no production account changed.",
"captured_by": "agent",
"source": "screencapture"
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"change":"Integration setup keeps checking after sign-in, offers retry when a check fails, and opens external settings in your browser. Delayed replies cannot change a page or account you have left."}
46 changes: 45 additions & 1 deletion app/docs/mobile-browser-auth.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,10 @@ statuses for other integrations. Task snapshots validate before cache adoption.
checks use that verdict, never the presence of a serialized access token.

Task authorization keeps the prior default until the server confirms the new
grant, then persists the selection. Failure to persist the default remains
grant, then persists the selection. A selection owns its attempt from the
confirmation dialog through launch, polling and save; choosing another app while
an earlier authorization is still launching retires it (latest selection wins).
A launch that throws reports failure and leaves no loading state behind. Failure to persist the default remains
retryable. An unfinished browser flow shows instructions and a manual status
check; it never becomes an endless spinner or a confirmed grant.

Expand All @@ -39,12 +42,17 @@ check; it never becomes an endless spinner or a confirmed grant.
- [x] Terminal, retryable read failures preserve saved connection information.
- [x] Fresh post-auth reads fence old reads and account changes.
- [x] Task selection follows server-confirmed authorization.
- [x] Latest task selection wins even while an older authorization is still
launching; late receipts after page or account changes do nothing.
- [x] Overlapping default saves land in choice order; only the latest publishes.
- [x] An MCP registration is reused only for its own URL, never after an edit.
- [x] Hermetic production-widget, native-bridge and provider regressions.
- [ ] Merge the fix and ship an Android store release (also validate iOS).
- [ ] On Samsung S26 Ultra, complete referral Google signup and return to Omi.
- [ ] Verify the reported integration(s) connect and display the server result.
- [ ] Identify the separate repeated Try Again screen with app version and a
screenshot or exact screen name; investigate any remaining failure.
- [ ] Keep #21378 open until the release ships and the customer confirms.

Featurebase incident: https://github.com/BasedHardware/omi/issues/21378.
The report does not identify the failing integration(s), app version or general
Expand All @@ -54,3 +62,39 @@ not. No account contents or recordings are changed by this fix.
References:
- https://pub.dev/packages/url_launcher#checking-supported-schemes
- https://developers.googleblog.com/upcoming-security-changes-to-googles-oauth-20-authorization-endpoint-in-embedded-webviews/

## Completion audit after #21381

The first PR was merged while the audit continued. The follow-up closes the
remaining mobile and web client gaps:

- Native Calendar/Gmail and task authorization keep checking when the grant is
saved after foreground return; default selection follows the confirmed grant.
- Marketplace setup uses a typed successful boolean verdict, serial probes,
bounded initial/detail reads and explicit retry. Failed responses and absent
endpoints cannot claim setup complete. Only explicit setup/enable intent may
auto-install after confirmation.
- MCP waits for approval AND the server's enable receipt, bounds creation/status
calls, retries an existing registration only for the URL it was made for,
stops waiting when the URL is edited, cancels on leaving and cannot pop a
different route from a late callback.
- External settings/home pages join affiliate signup in the system browser;
embedded Google popup handling and spoofed user agents are retired. URL/uid
construction, server-stored grants and conversation data are preserved.
- Vendor completion probes use the documented uid contract without forwarding
Omi's Firebase bearer token; vendor failures cannot expire the Omi session.
- Web referral activation has one token/request/body deadline, a working manual
retry and account/route ownership fences. It does not automatically retry an
eligibility rejection or alter the desktop referral destination.

Release requires the follow-up mobile AND web changes, plus #21355's web helper
proxy/client together. As of 2026-10-16, #21355 is open (mergeable, CI green,
no approval); its review notes that the callback configuration and a real
provider round trip are not yet demonstrated. Roll out the web helper proxy and
this client in one coordinated deploy, never this client alone. Verify the
Firebase/Google/Apple callback configuration in `web/app/docs/firebase-auth.md`
from #21355 and perform real Google and Apple sign-ins, including on Samsung
S26 Ultra. Mocked and hermetic tests do not verify production sign-in.
The unspecified general Try Again screen still requires the reporter's screen,
version and integration names; this audit does not invent its cause or close the
customer incident from code merge.
2 changes: 2 additions & 0 deletions app/e2e/flows/app-detail.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ name: app-detail
description: Apps → App Detail — view app info, reviews, capabilities, install/enable toggle
covers:
- app/lib/pages/apps/app_detail/app_detail.dart
- app/lib/pages/apps/app_detail/app_setup_owner.dart
- app/lib/pages/apps/widgets/omi_external_browser_page.dart
- app/lib/pages/apps/app_detail/reviews_section.dart
- app/lib/pages/apps/app_detail/reviews_list_page.dart
- app/lib/pages/apps/page.dart
Expand Down
1 change: 1 addition & 0 deletions app/e2e/flows/referral-program.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ name: referral-program
description: Settings → Referral Program → system browser signup → return to Omi
covers:
- app/lib/pages/referral/referral_page.dart
- app/lib/pages/apps/widgets/omi_external_browser_page.dart
- app/lib/pages/settings/settings_drawer.dart
prerequisites:
- auth_ready # User completed real sign-in (Google/Apple) and onboarding. Android: `bash setup.sh android`. iOS: `bash setup.sh ios`. App must show home screen.
Expand Down
52 changes: 33 additions & 19 deletions app/lib/backend/http/api/apps.dart
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import 'dart:convert';

Check warning on line 1 in app/lib/backend/http/api/apps.dart

View workflow job for this annotation

GitHub Actions / Hygiene

Large changed file

app/lib/backend/http/api/apps.dart is 838 lines; consider splitting files over 800 lines.
import 'dart:developer';
import 'dart:io';

import 'package:omi/backend/http/api_result.dart';
import 'package:omi/backend/http/http_pool_manager.dart';
import 'package:http/http.dart' as http;
import 'package:omi/backend/http/shared.dart';
import 'package:omi/backend/preferences.dart';
import 'package:omi/backend/schema/app.dart';
Expand Down Expand Up @@ -408,26 +410,38 @@
return '${head.substring(0, queryIndex)}?${[...kept, uidParam].join('&')}$fragment';
}

Future<bool> isAppSetupCompleted(String? url) async {
if (url == null || url.isEmpty) return true;
Logger.debug('isAppSetupCompleted: $url');
var response = await makeApiCall(
url: appSetupUrlWithUid(url, SharedPreferencesUtil().uid),
method: 'GET',
headers: {},
body: '',
enum AppSetupStatus { completed, pending, notRequired }

Future<ApiResult<AppSetupStatus>> getAppSetupStatus(String? url, {ApiSend? send, String? uid}) {
if (url == null || url.isEmpty) return Future.value(const ApiSuccess(AppSetupStatus.notRequired));
final target = Uri.tryParse(url);
final omi = send == null ? Uri.tryParse(Env.apiBaseUrl ?? '') : null;
final isOmiOrigin = target != null &&
omi != null &&
omi.host.isNotEmpty &&
target.scheme == omi.scheme &&
target.host == omi.host &&
target.port == omi.port;
// Vendor endpoints implement a uid-based completion probe. A vendor 401 must
// not refresh/expire the Omi session or receive its Firebase bearer token.
final ApiSend? transport = send ??
(isOmiOrigin
? null
: (request) => HttpPoolManager.instance.send(() => http.Request(request.method, Uri.parse(request.url)),
timeout: const Duration(seconds: 15), retries: 0));
final account = uid ?? SharedPreferencesUtil().uid;
return executeApi(
canSend: send == null ? () => SharedPreferencesUtil().uid == account : null,
request: ApiRequest(url: appSetupUrlWithUid(url, account), method: 'GET'),
send: transport,
decode: (body) {
final data = jsonDecode(body);
if (data is! Map || data['is_setup_completed'] is! bool) {
throw const FormatException('Invalid setup status');
}
return data['is_setup_completed'] == true ? AppSetupStatus.completed : AppSetupStatus.pending;
},
);
try {
final data = jsonDecode(response?.body ?? '{}') as Map<String, dynamic>;
Logger.debug(data);
return data['is_setup_completed'] ?? false;
} on FormatException catch (e) {
Logger.debug('Response not a valid json: $e');
return false;
} catch (e) {
Logger.debug('Error triggering request at endpoint: $e');
return false;
}
}

Future<(bool, String, String?)> submitAppServer(File file, Map<String, dynamic> appData) async {
Expand Down
Loading
Loading