@livx.cc/appwrap 0.42.4 → 0.46.1
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/package.json +4 -2
- package/runtime/app/app.ts +15 -0
- package/runtime/app/main-page.ts +11 -2
- package/runtime/app/shell/custom-webview.ios.ts +18 -8
- package/runtime/app/shell/handlers-background.ts +1 -1
- package/runtime/app/shell/web-quirks.ts +63 -0
- package/runtime-desktop/autotest/index.html +77 -0
- package/runtime-desktop/bridge-shim/build.sh +9 -0
- package/runtime-desktop/bridge-shim/content-entry.ts +87 -0
- package/runtime-desktop/bridge-shim/shim.ts +317 -0
- package/runtime-desktop/src-tauri/Cargo.lock +5461 -0
- package/runtime-desktop/src-tauri/Cargo.toml +40 -0
- package/runtime-desktop/src-tauri/build.rs +3 -0
- package/runtime-desktop/src-tauri/capabilities/default.json +6 -0
- package/runtime-desktop/src-tauri/icons/icon.png +0 -0
- package/runtime-desktop/src-tauri/shell_config.json +10 -0
- package/runtime-desktop/src-tauri/src/biometrics_mac.rs +138 -0
- package/runtime-desktop/src-tauri/src/bridge_mac.rs +664 -0
- package/runtime-desktop/src-tauri/src/main.rs +879 -0
- package/runtime-desktop/src-tauri/src/notifications_mac.rs +202 -0
- package/runtime-desktop/src-tauri/src/oauth_mac.rs +139 -0
- package/runtime-desktop/src-tauri/src/popup_mac.rs +519 -0
- package/runtime-desktop/src-tauri/src/push_mac.rs +193 -0
- package/runtime-desktop/src-tauri/src/server.rs +156 -0
- package/runtime-desktop/src-tauri/src/sidecar.rs +398 -0
- package/runtime-desktop/src-tauri/tauri.conf.json +13 -0
- package/scripts/stage-assets.mjs +4 -2
- package/src/cli.ts +664 -37
- package/src/config.ts +76 -1
- package/src/desktop.ts +293 -0
- package/src/handlers.ts +71 -0
- package/src/icon.ts +207 -0
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
// macOS local notifications (notifications.requestPermission / .schedule / .pending) via
|
|
2
|
+
// UNUserNotificationCenter.
|
|
3
|
+
//
|
|
4
|
+
// Why not tauri-plugin-notification: its desktop path is notify-rust (fire-and-forget), which has NO
|
|
5
|
+
// click callback and no real scheduling — so a tapped notification can never route a deep link. We
|
|
6
|
+
// post local notifications ourselves through UNUserNotificationCenter and install a delegate whose
|
|
7
|
+
// `didReceiveNotificationResponse` reads the `deepLink` from the notification's userInfo and routes it
|
|
8
|
+
// through the SAME warm/cold `deeplink.open` path as an OS open (crate::route_deeplink). A tap without
|
|
9
|
+
// a deepLink just activates the app (no event), matching the contract.
|
|
10
|
+
//
|
|
11
|
+
// Delivery caveat: UNUserNotificationCenter only delivers from a bundled `.app` with a bundle id. Under
|
|
12
|
+
// an unbundled `cargo run` (dev) authorization/scheduling may no-op — the built `.app` is the real
|
|
13
|
+
// surface. The kit contract is unchanged: the demo still calls `schedule({ ..., deepLink })`.
|
|
14
|
+
use std::sync::atomic::{AtomicBool, Ordering};
|
|
15
|
+
use std::sync::Mutex;
|
|
16
|
+
|
|
17
|
+
use block2::RcBlock;
|
|
18
|
+
use objc2::rc::Retained;
|
|
19
|
+
use objc2::runtime::{Bool, NSObject, NSObjectProtocol, ProtocolObject};
|
|
20
|
+
use objc2::{define_class, msg_send, MainThreadMarker, MainThreadOnly};
|
|
21
|
+
use objc2_foundation::{ns_string, NSArray, NSDictionary, NSError, NSString};
|
|
22
|
+
use objc2_user_notifications::{
|
|
23
|
+
UNAuthorizationOptions, UNMutableNotificationContent, UNNotification, UNNotificationPresentationOptions,
|
|
24
|
+
UNNotificationRequest, UNNotificationResponse, UNTimeIntervalNotificationTrigger,
|
|
25
|
+
UNUserNotificationCenter, UNUserNotificationCenterDelegate,
|
|
26
|
+
};
|
|
27
|
+
use serde_json::{json, Value};
|
|
28
|
+
|
|
29
|
+
use crate::{deliver, route_deeplink};
|
|
30
|
+
|
|
31
|
+
// The notification-center delegate — a plain NSObject subclass conforming to the delegate protocol.
|
|
32
|
+
// No ivars: taps route through the global `route_deeplink` (which reaches the main window via the
|
|
33
|
+
// stashed AppHandle), so the delegate holds no state.
|
|
34
|
+
define_class!(
|
|
35
|
+
#[unsafe(super(NSObject))]
|
|
36
|
+
#[thread_kind = MainThreadOnly]
|
|
37
|
+
#[name = "AppwrapUNDelegate"]
|
|
38
|
+
struct UNDelegate;
|
|
39
|
+
|
|
40
|
+
unsafe impl NSObjectProtocol for UNDelegate {}
|
|
41
|
+
|
|
42
|
+
unsafe impl UNUserNotificationCenterDelegate for UNDelegate {
|
|
43
|
+
// Show the banner even when the app is frontmost (default macOS behaviour is to suppress a
|
|
44
|
+
// notification whose app is active) so a scheduled notification is visible + tappable in-app.
|
|
45
|
+
#[unsafe(method(userNotificationCenter:willPresentNotification:withCompletionHandler:))]
|
|
46
|
+
fn will_present(
|
|
47
|
+
&self,
|
|
48
|
+
_center: &UNUserNotificationCenter,
|
|
49
|
+
_notification: &UNNotification,
|
|
50
|
+
completion: &block2::DynBlock<dyn Fn(UNNotificationPresentationOptions)>,
|
|
51
|
+
) {
|
|
52
|
+
completion.call((UNNotificationPresentationOptions::Banner | UNNotificationPresentationOptions::Sound,));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// A notification was tapped. If it carries a `deepLink` in userInfo, route it through the same
|
|
56
|
+
// warm/cold path as an OS deep-link open; otherwise the app just activates (no event).
|
|
57
|
+
#[unsafe(method(userNotificationCenter:didReceiveNotificationResponse:withCompletionHandler:))]
|
|
58
|
+
fn did_receive(
|
|
59
|
+
&self,
|
|
60
|
+
_center: &UNUserNotificationCenter,
|
|
61
|
+
response: &UNNotificationResponse,
|
|
62
|
+
completion: &block2::DynBlock<dyn Fn()>,
|
|
63
|
+
) {
|
|
64
|
+
let user_info = response.notification().request().content().userInfo();
|
|
65
|
+
let val: *mut NSString = unsafe { msg_send![&*user_info, objectForKey: ns_string!("deepLink")] };
|
|
66
|
+
if !val.is_null() {
|
|
67
|
+
let url = unsafe { (*val).to_string() };
|
|
68
|
+
if !url.is_empty() {
|
|
69
|
+
eprintln!("[notify] tapped → deeplink.open ← {url}");
|
|
70
|
+
route_deeplink(&url);
|
|
71
|
+
} else {
|
|
72
|
+
eprintln!("[notify] tapped (empty deepLink) — activate only");
|
|
73
|
+
}
|
|
74
|
+
} else {
|
|
75
|
+
eprintln!("[notify] tapped (no deepLink) — activate only");
|
|
76
|
+
}
|
|
77
|
+
completion.call(());
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
/// Strong ref to the delegate — UNUserNotificationCenter holds it WEAKLY, so ARC would free it the
|
|
83
|
+
/// instant this drops and taps would stop routing. Parked for the app's lifetime. Only ever touched on
|
|
84
|
+
/// the main thread; the Send impl exists purely so it can live in a static.
|
|
85
|
+
struct DelegateHolder(Option<Retained<UNDelegate>>);
|
|
86
|
+
unsafe impl Send for DelegateHolder {}
|
|
87
|
+
static DELEGATE: Mutex<DelegateHolder> = Mutex::new(DelegateHolder(None));
|
|
88
|
+
static DELEGATE_SET: AtomicBool = AtomicBool::new(false);
|
|
89
|
+
|
|
90
|
+
/// Install the notification-center delegate once (idempotent). Must run on the main thread. Called at
|
|
91
|
+
/// setup (so a cold tap right after launch is captured) and defensively before schedule.
|
|
92
|
+
pub fn ensure_delegate(mtm: MainThreadMarker) {
|
|
93
|
+
if DELEGATE_SET.swap(true, Ordering::SeqCst) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
let delegate: Retained<UNDelegate> = unsafe { msg_send![UNDelegate::alloc(mtm), init] };
|
|
97
|
+
let center = UNUserNotificationCenter::currentNotificationCenter();
|
|
98
|
+
center.setDelegate(Some(ProtocolObject::from_ref(&*delegate)));
|
|
99
|
+
DELEGATE.lock().unwrap().0 = Some(delegate);
|
|
100
|
+
eprintln!("[notify] UNUserNotificationCenter delegate installed");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/// `notifications.requestPermission` → 'granted' | 'denied'. Replies from the authorization block.
|
|
104
|
+
pub fn request_permission(window: tauri::WebviewWindow, id: String) {
|
|
105
|
+
let w = window.clone();
|
|
106
|
+
let fail_id = id.clone();
|
|
107
|
+
let res = window.run_on_main_thread(move || {
|
|
108
|
+
let mtm = MainThreadMarker::new().expect("run_on_main_thread guarantees main");
|
|
109
|
+
ensure_delegate(mtm);
|
|
110
|
+
let center = UNUserNotificationCenter::currentNotificationCenter();
|
|
111
|
+
let opts = UNAuthorizationOptions::Alert | UNAuthorizationOptions::Sound | UNAuthorizationOptions::Badge;
|
|
112
|
+
let cb_window = w.clone();
|
|
113
|
+
let cb_id = id.clone();
|
|
114
|
+
let block = RcBlock::new(move |granted: Bool, error: *mut NSError| {
|
|
115
|
+
if !error.is_null() {
|
|
116
|
+
eprintln!("[notify] requestAuthorization error: {}", unsafe { (*error).localizedDescription() });
|
|
117
|
+
}
|
|
118
|
+
let status = if granted.as_bool() { "granted" } else { "denied" };
|
|
119
|
+
deliver(&cb_window, &json!({ "v": 1, "id": cb_id, "kind": "response", "result": status }));
|
|
120
|
+
});
|
|
121
|
+
center.requestAuthorizationWithOptions_completionHandler(opts, &block);
|
|
122
|
+
});
|
|
123
|
+
if let Err(e) = res {
|
|
124
|
+
deliver(&window, &json!({ "v": 1, "id": fail_id, "kind": "response",
|
|
125
|
+
"error": { "code": "NATIVE_ERROR", "message": format!("failed to reach main thread: {e}") } }));
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/// `notifications.schedule` — post a local notification after `delaySec` (min 1), carrying an optional
|
|
130
|
+
/// `deepLink` in userInfo. Replies { id } once the request is accepted by the center.
|
|
131
|
+
pub fn schedule(window: tauri::WebviewWindow, id: String, params: Value) {
|
|
132
|
+
// Kit ScheduleOptions: { id?, title, body?, delaySec?, deepLink? }. `id` (notification id) is a
|
|
133
|
+
// number in the kit; fall back to a timestamp so each schedule is distinct.
|
|
134
|
+
let notif_id = params["id"].as_i64().unwrap_or_else(|| {
|
|
135
|
+
std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.as_millis() as i64).unwrap_or(0)
|
|
136
|
+
});
|
|
137
|
+
let title = params["title"].as_str().unwrap_or("").to_string();
|
|
138
|
+
let body = params["body"].as_str().unwrap_or("").to_string();
|
|
139
|
+
let delay = params["delaySec"].as_f64().unwrap_or(1.0).max(1.0);
|
|
140
|
+
let deep_link = params["deepLink"].as_str().unwrap_or("").to_string();
|
|
141
|
+
|
|
142
|
+
let w = window.clone();
|
|
143
|
+
let fail_id = id.clone();
|
|
144
|
+
let res = window.run_on_main_thread(move || {
|
|
145
|
+
let mtm = MainThreadMarker::new().expect("run_on_main_thread guarantees main");
|
|
146
|
+
ensure_delegate(mtm);
|
|
147
|
+
let content = UNMutableNotificationContent::new();
|
|
148
|
+
content.setTitle(&NSString::from_str(&title));
|
|
149
|
+
content.setBody(&NSString::from_str(&body));
|
|
150
|
+
if !deep_link.is_empty() {
|
|
151
|
+
let dl = NSString::from_str(&deep_link);
|
|
152
|
+
let user_info = NSDictionary::from_slices(&[ns_string!("deepLink")], &[&*dl]);
|
|
153
|
+
let _: () = unsafe { msg_send![&*content, setUserInfo: &*user_info] };
|
|
154
|
+
}
|
|
155
|
+
let trigger = UNTimeIntervalNotificationTrigger::triggerWithTimeInterval_repeats(delay, false);
|
|
156
|
+
let ident = NSString::from_str(&format!("appwrap-{notif_id}"));
|
|
157
|
+
let request = UNNotificationRequest::requestWithIdentifier_content_trigger(
|
|
158
|
+
&ident,
|
|
159
|
+
&content,
|
|
160
|
+
Some(trigger.as_ref()),
|
|
161
|
+
);
|
|
162
|
+
let cb_window = w.clone();
|
|
163
|
+
let cb_id = id.clone();
|
|
164
|
+
let block = RcBlock::new(move |error: *mut NSError| {
|
|
165
|
+
if error.is_null() {
|
|
166
|
+
deliver(&cb_window, &json!({ "v": 1, "id": cb_id, "kind": "response", "result": { "id": notif_id } }));
|
|
167
|
+
} else {
|
|
168
|
+
let msg = unsafe { (*error).localizedDescription() }.to_string();
|
|
169
|
+
eprintln!("[notify] schedule failed: {msg}");
|
|
170
|
+
deliver(&cb_window, &json!({ "v": 1, "id": cb_id, "kind": "response",
|
|
171
|
+
"error": { "code": "NATIVE_ERROR", "message": msg } }));
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
let center = UNUserNotificationCenter::currentNotificationCenter();
|
|
175
|
+
center.addNotificationRequest_withCompletionHandler(&request, Some(&block));
|
|
176
|
+
eprintln!("[notify] scheduled #{notif_id} in {delay}s{}", if deep_link.is_empty() { String::new() } else { format!(" → {deep_link}") });
|
|
177
|
+
});
|
|
178
|
+
if let Err(e) = res {
|
|
179
|
+
deliver(&window, &json!({ "v": 1, "id": fail_id, "kind": "response",
|
|
180
|
+
"error": { "code": "NATIVE_ERROR", "message": format!("failed to reach main thread: {e}") } }));
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/// `notifications.pending` → count of scheduled-but-not-delivered notifications.
|
|
185
|
+
pub fn pending(window: tauri::WebviewWindow, id: String) {
|
|
186
|
+
let w = window.clone();
|
|
187
|
+
let fail_id = id.clone();
|
|
188
|
+
let res = window.run_on_main_thread(move || {
|
|
189
|
+
let center = UNUserNotificationCenter::currentNotificationCenter();
|
|
190
|
+
let cb_window = w.clone();
|
|
191
|
+
let cb_id = id.clone();
|
|
192
|
+
let block = RcBlock::new(move |requests: std::ptr::NonNull<NSArray<UNNotificationRequest>>| {
|
|
193
|
+
let count = unsafe { requests.as_ref() }.count();
|
|
194
|
+
deliver(&cb_window, &json!({ "v": 1, "id": cb_id, "kind": "response", "result": count }));
|
|
195
|
+
});
|
|
196
|
+
center.getPendingNotificationRequestsWithCompletionHandler(&block);
|
|
197
|
+
});
|
|
198
|
+
if let Err(e) = res {
|
|
199
|
+
deliver(&window, &json!({ "v": 1, "id": fail_id, "kind": "response",
|
|
200
|
+
"error": { "code": "NATIVE_ERROR", "message": format!("failed to reach main thread: {e}") } }));
|
|
201
|
+
}
|
|
202
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
// macOS `oauth.authorize` — ASWebAuthenticationSession (system-browser auth sheet), exact parity with
|
|
2
|
+
// the iOS shell path in runtime/app/shell/handlers-oauth.ts: the session captures the dynamic
|
|
3
|
+
// `callbackScheme` redirect ITSELF (no Info.plist registration — reversed Google client ids aren't
|
|
4
|
+
// registrable), auto-closes on redirect, and signals user-cancel as error code 1 (CanceledLogin).
|
|
5
|
+
//
|
|
6
|
+
// Threading: everything AS* runs on the main thread (run_on_main_thread). The strong refs to the
|
|
7
|
+
// session + presentation-context provider are parked in a static for the sheet's lifetime — ARC frees
|
|
8
|
+
// them the moment the locals drop otherwise, which silently cancels the flow mid-handshake (same
|
|
9
|
+
// warning as the iOS file).
|
|
10
|
+
use std::sync::atomic::Ordering;
|
|
11
|
+
use std::sync::Mutex;
|
|
12
|
+
|
|
13
|
+
use objc2::rc::Retained;
|
|
14
|
+
use objc2::runtime::{NSObject, NSObjectProtocol, ProtocolObject};
|
|
15
|
+
use objc2::{define_class, AnyThread, DefinedClass, MainThreadOnly};
|
|
16
|
+
use objc2_authentication_services::{
|
|
17
|
+
ASPresentationAnchor, ASWebAuthenticationPresentationContextProviding, ASWebAuthenticationSession,
|
|
18
|
+
};
|
|
19
|
+
use objc2_foundation::{NSError, NSString, NSURL};
|
|
20
|
+
use serde_json::json;
|
|
21
|
+
|
|
22
|
+
use crate::{deliver, deliver_error, OAUTH_IN_FLIGHT};
|
|
23
|
+
|
|
24
|
+
pub struct ProviderIvars {
|
|
25
|
+
/// The shell window's NSWindow, retained as NSObject (ASPresentationAnchor aliases NSObject in
|
|
26
|
+
/// the objc2 bindings; on macOS the real type is NSWindow).
|
|
27
|
+
anchor: Retained<NSObject>,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
define_class!(
|
|
31
|
+
#[unsafe(super(NSObject))]
|
|
32
|
+
#[thread_kind = MainThreadOnly]
|
|
33
|
+
#[name = "AppwrapOAuthContextProvider"]
|
|
34
|
+
#[ivars = ProviderIvars]
|
|
35
|
+
struct Provider;
|
|
36
|
+
|
|
37
|
+
unsafe impl NSObjectProtocol for Provider {}
|
|
38
|
+
|
|
39
|
+
unsafe impl ASWebAuthenticationPresentationContextProviding for Provider {
|
|
40
|
+
#[unsafe(method_id(presentationAnchorForWebAuthenticationSession:))]
|
|
41
|
+
fn presentation_anchor(
|
|
42
|
+
&self,
|
|
43
|
+
_session: &ASWebAuthenticationSession,
|
|
44
|
+
) -> Retained<ASPresentationAnchor> {
|
|
45
|
+
self.ivars().anchor.clone()
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
/// Strong refs held while the sheet is up. Only ever touched on the main thread — the Send impl
|
|
51
|
+
/// exists purely so the (main-thread-confined) pair can live in a static Mutex.
|
|
52
|
+
struct ActiveFlow(Option<(Retained<ASWebAuthenticationSession>, Retained<Provider>)>);
|
|
53
|
+
unsafe impl Send for ActiveFlow {}
|
|
54
|
+
static ACTIVE: Mutex<ActiveFlow> = Mutex::new(ActiveFlow(None));
|
|
55
|
+
|
|
56
|
+
/// Kick off the auth session. Caller has already set OAUTH_IN_FLIGHT; every exit path here (start
|
|
57
|
+
/// failure or completion) clears it.
|
|
58
|
+
pub fn start(window: tauri::WebviewWindow, id: String, url: String, scheme: String, ephemeral: bool) {
|
|
59
|
+
let w = window.clone();
|
|
60
|
+
let res = window.run_on_main_thread(move || start_on_main(w, id.clone(), &url, &scheme, ephemeral, &id));
|
|
61
|
+
if let Err(e) = res {
|
|
62
|
+
OAUTH_IN_FLIGHT.store(false, Ordering::SeqCst);
|
|
63
|
+
eprintln!("[oauth] failed to reach main thread: {e}");
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
fn fail(window: &tauri::WebviewWindow, id: &str, msg: &str) {
|
|
68
|
+
OAUTH_IN_FLIGHT.store(false, Ordering::SeqCst);
|
|
69
|
+
deliver_error(window, id, "NATIVE_ERROR", msg);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
fn start_on_main(window: tauri::WebviewWindow, id: String, url: &str, scheme: &str, ephemeral: bool, id_ref: &str) {
|
|
73
|
+
let mtm = objc2::MainThreadMarker::new().expect("run_on_main_thread guarantees main");
|
|
74
|
+
|
|
75
|
+
let Some(ns_url) = NSURL::URLWithString(&NSString::from_str(url)) else {
|
|
76
|
+
return fail(&window, id_ref, "oauth.authorize: invalid url");
|
|
77
|
+
};
|
|
78
|
+
let ns_window = match window.ns_window() {
|
|
79
|
+
Ok(p) if !p.is_null() => p,
|
|
80
|
+
_ => return fail(&window, id_ref, "oauth.authorize: no native window"),
|
|
81
|
+
};
|
|
82
|
+
// Retain the NSWindow (as NSObject) for the provider's lifetime.
|
|
83
|
+
let anchor: Retained<NSObject> =
|
|
84
|
+
unsafe { Retained::retain(ns_window as *mut NSObject) }.expect("non-null NSWindow");
|
|
85
|
+
|
|
86
|
+
let completion_window = window.clone();
|
|
87
|
+
let completion_id = id.clone();
|
|
88
|
+
let block = block2::RcBlock::new(move |cb_url: *mut NSURL, error: *mut NSError| {
|
|
89
|
+
// Drop the strong refs + the in-flight guard FIRST — a follow-up authorize must be legal
|
|
90
|
+
// from inside the completion.
|
|
91
|
+
ACTIVE.lock().unwrap().0 = None;
|
|
92
|
+
OAUTH_IN_FLIGHT.store(false, Ordering::SeqCst);
|
|
93
|
+
if !error.is_null() {
|
|
94
|
+
let err = unsafe { &*error };
|
|
95
|
+
// ASWebAuthenticationSessionErrorCodeCanceledLogin == 1 → user dismissed the sheet.
|
|
96
|
+
let code = if err.code() == 1 { "CANCELLED" } else { "NATIVE_ERROR" };
|
|
97
|
+
let msg = err.localizedDescription().to_string();
|
|
98
|
+
eprintln!("[oauth] completed with error ({code}): {msg}");
|
|
99
|
+
deliver_error(&completion_window, &completion_id, code, &msg);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
let url_str = if cb_url.is_null() {
|
|
103
|
+
String::new()
|
|
104
|
+
} else {
|
|
105
|
+
unsafe { (*cb_url).absoluteString() }.map(|s| s.to_string()).unwrap_or_default()
|
|
106
|
+
};
|
|
107
|
+
eprintln!("[oauth] completed: callback url captured");
|
|
108
|
+
let resp = json!({ "v": 1, "id": completion_id, "kind": "response", "result": { "url": url_str } });
|
|
109
|
+
deliver(&completion_window, &resp);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// The callbackURLScheme initializer is soft-deprecated in favor of the ASWebAuthenticationSession-
|
|
113
|
+
// Callback variant (macOS 14+); the scheme form works from 10.15 and matches the iOS shell exactly.
|
|
114
|
+
#[allow(deprecated)]
|
|
115
|
+
let session = unsafe {
|
|
116
|
+
ASWebAuthenticationSession::initWithURL_callbackURLScheme_completionHandler(
|
|
117
|
+
ASWebAuthenticationSession::alloc(),
|
|
118
|
+
&ns_url,
|
|
119
|
+
Some(&NSString::from_str(scheme)),
|
|
120
|
+
&*block as *const _ as *mut _,
|
|
121
|
+
)
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
let provider = Provider::alloc(mtm).set_ivars(ProviderIvars { anchor });
|
|
125
|
+
let provider: Retained<Provider> = unsafe { objc2::msg_send![super(provider), init] };
|
|
126
|
+
unsafe {
|
|
127
|
+
session.setPresentationContextProvider(Some(ProtocolObject::from_ref(&*provider)));
|
|
128
|
+
session.setPrefersEphemeralWebBrowserSession(ephemeral);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Park strong refs BEFORE start() — the completion can fire synchronously on failure.
|
|
132
|
+
ACTIVE.lock().unwrap().0 = Some((session.clone(), provider));
|
|
133
|
+
let started = unsafe { session.start() };
|
|
134
|
+
eprintln!("[oauth] session start → {started}");
|
|
135
|
+
if !started {
|
|
136
|
+
ACTIVE.lock().unwrap().0 = None;
|
|
137
|
+
fail(&window, id_ref, "oauth.authorize: session failed to start");
|
|
138
|
+
}
|
|
139
|
+
}
|