Skip to main content

fission_core/
context.rs

1//! Reducer context and effect builder.
2//!
3//! When a reducer needs to emit side-effects or inspect the [`ActionInput`]
4//! that triggered it, it receives a [`ReducerContext`]. The context provides
5//! an [`Effects`] builder for issuing capabilities, jobs, services, and
6//! runtime-control effects plus binding callback actions.
7
8use crate::action::{Action, ActionEnvelope, ActionId, GlobalState};
9use crate::async_runtime::{
10    JobRef, JobRequestPayload, JobSpec, ServiceBindings, ServiceCommandPayload, ServiceSlot,
11    ServiceSpec, ServiceStartPayload, ServiceStopPayload,
12};
13use crate::capability::{
14    CapabilityInvocationPayload, CapabilityType, OperationCapability, OperationCapabilityInvocation,
15};
16use crate::effect::{ActionInput, Effect, EffectEnvelope, RuntimeEffect, ScrollIntoViewRequest};
17use crate::platform::{
18    CancelNotificationRequest, NotificationPermissionRequest, NotificationRequest,
19    PushRegistrationRequest, SetBadgeCountRequest, CANCEL_ALL_NOTIFICATIONS, CANCEL_NOTIFICATION,
20    GET_NOTIFICATION_SETTINGS, REGISTER_PUSH_NOTIFICATIONS, REQUEST_NOTIFICATION_PERMISSION,
21    SCHEDULE_NOTIFICATION, SET_BADGE_COUNT, SHOW_NOTIFICATION, UNREGISTER_PUSH_NOTIFICATIONS,
22};
23use crate::platform_barcode::{
24    BarcodeImageDecodeRequest, BarcodeScanRequest, CANCEL_BARCODE_SCAN, DECODE_BARCODE_IMAGE,
25    SCAN_BARCODE,
26};
27use crate::platform_biometric::{
28    BiometricAuthenticateRequest, AUTHENTICATE_BIOMETRIC, CANCEL_BIOMETRIC_AUTHENTICATION,
29    GET_BIOMETRIC_AVAILABILITY,
30};
31use crate::platform_bluetooth::{
32    BluetoothAdvertiseRequest, BluetoothConnectRequest, BluetoothDisconnectRequest,
33    BluetoothPermissionRequest, BluetoothReadRequest, BluetoothScanRequest,
34    BluetoothStopAdvertiseRequest, BluetoothWriteRequest, CONNECT_BLUETOOTH_DEVICE,
35    DISCONNECT_BLUETOOTH_DEVICE, GET_BLUETOOTH_AVAILABILITY, READ_BLUETOOTH_CHARACTERISTIC,
36    REQUEST_BLUETOOTH_PERMISSION, SCAN_BLUETOOTH_DEVICES, START_BLUETOOTH_ADVERTISING,
37    STOP_BLUETOOTH_ADVERTISING, WRITE_BLUETOOTH_CHARACTERISTIC,
38};
39use crate::platform_camera::{
40    CameraCaptureRequest, CameraFlashlightRequest, CameraPermissionRequest, CANCEL_CAMERA_CAPTURE,
41    CAPTURE_PHOTO, GET_CAMERA_AVAILABILITY, REQUEST_CAMERA_PERMISSION, SET_CAMERA_FLASHLIGHT,
42};
43use crate::platform_clipboard::{
44    ClipboardContent, ClipboardWriteTextRequest, CLEAR_CLIPBOARD, READ_CLIPBOARD_CONTENT,
45    READ_CLIPBOARD_TEXT, WRITE_CLIPBOARD_CONTENT, WRITE_CLIPBOARD_TEXT,
46};
47use crate::platform_geolocation::{
48    GeolocationPermissionRequest, GeolocationPositionRequest, GET_CURRENT_POSITION,
49    GET_GEOLOCATION_PERMISSION, REQUEST_GEOLOCATION_PERMISSION,
50};
51use crate::platform_haptics::{
52    HapticImpactRequest, HapticNotificationRequest, HapticPatternRequest, HAPTIC_IMPACT,
53    HAPTIC_NOTIFICATION, HAPTIC_PATTERN, HAPTIC_SELECTION,
54};
55use crate::platform_microphone::{
56    MicrophoneCaptureRequest, MicrophonePermissionRequest, CANCEL_MICROPHONE_CAPTURE,
57    CAPTURE_MICROPHONE_AUDIO, GET_MICROPHONE_AVAILABILITY, REQUEST_MICROPHONE_PERMISSION,
58};
59use crate::platform_nfc::{
60    NfcEmulationRequest, NfcScanRequest, NfcWriteRequest, CANCEL_NFC_SESSION, EMULATE_NFC_TAG,
61    GET_NFC_AVAILABILITY, SCAN_NFC_TAG, WRITE_NFC_TAG,
62};
63use crate::platform_passkey::{
64    PasskeyAuthenticationRequest, PasskeyRegistrationRequest, AUTHENTICATE_PASSKEY,
65    CANCEL_PASSKEY_OPERATION, GET_PASSKEY_AVAILABILITY, REGISTER_PASSKEY,
66};
67use crate::platform_volume::{
68    VolumeAdjustRequest, VolumeSetRequest, VolumeStream, ADJUST_VOLUME_LEVEL, GET_VOLUME_LEVEL,
69    SET_VOLUME_LEVEL,
70};
71use crate::platform_wifi::{
72    WifiConnectRequest, WifiDisconnectRequest, WifiPermissionRequest, WifiScanRequest,
73    CONNECT_WIFI_NETWORK, DISCONNECT_WIFI_NETWORK, GET_WIFI_AVAILABILITY, REQUEST_WIFI_PERMISSION,
74    SCAN_WIFI_NETWORKS,
75};
76use crate::registry::{ActionRegistry, IntoHandler};
77use crate::EffectCallbackRegistry;
78use std::{
79    marker::PhantomData,
80    sync::{
81        atomic::{AtomicU64, Ordering},
82        Arc,
83    },
84};
85
86static NEXT_EFFECT_CALLBACK_ID: AtomicU64 = AtomicU64::new(1);
87
88/// The context passed to modern 3-argument reducer handlers.
89///
90/// Provides access to the [`Effects`] builder (for emitting side-effects) and
91/// the [`ActionInput`] that accompanied the dispatch (e.g. effect results,
92/// pointer coordinates, drop payloads).
93///
94/// # Example
95///
96/// ```rust,ignore
97/// fn handle_click(
98///     state: &mut GlobalState,
99///     action: ClickAction,
100///     ctx: &mut ReducerContext<GlobalState>,
101/// ) {
102///     // Read pointer position from the input
103///     if let Some((x, y, _, _)) = ctx.input.as_pointer() {
104///         state.last_click = (x, y);
105///     }
106///     // Issue a capability effect
107///     ctx.effects.capability(MY_CAPABILITY, request);
108/// }
109/// ```
110pub struct ReducerContext<'a, 'b, 'c, S: GlobalState> {
111    /// Mutable reference to the effects builder.
112    pub effects: &'a mut Effects<'b, S>,
113    /// The input data that accompanied this action dispatch.
114    pub input: &'c ActionInput,
115}
116
117/// Builder for emitting side-effects from within a reducer.
118///
119/// `Effects` accumulates [`EffectEnvelope`] values that the runtime collects
120/// after the reducer returns. Each effect can carry optional `on_ok` and
121/// `on_err` callbacks.
122///
123/// # Example
124///
125/// ```rust,ignore
126/// fn handle_save(
127///     state: &mut MyState,
128///     _action: Save,
129///     ctx: &mut ReducerContext<MyState>,
130/// ) {
131///     ctx.effects.capability(MY_CAPABILITY, request)
132///         .on_ok(ctx.effects.bind(SaveOk, handle_save_ok as fn(&mut MyState, SaveOk)))
133///         .on_err(ctx.effects.bind(SaveErr, handle_save_err as fn(&mut MyState, SaveErr)));
134/// }
135/// ```
136pub struct Effects<'a, S: GlobalState> {
137    /// Accumulated effect envelopes, drained by the runtime after the reducer.
138    pub out: Vec<EffectEnvelope>,
139    next_req_id: u64,
140    pub(crate) registry: Option<&'a mut ActionRegistry<S>>,
141    callback_registry: Option<Arc<EffectCallbackRegistry>>,
142    _phantom: PhantomData<S>,
143}
144
145impl<'a, S: GlobalState> Effects<'a, S> {
146    pub fn new(next_req_id: u64, registry: &'a mut ActionRegistry<S>) -> Self {
147        Self {
148            out: Vec::new(),
149            next_req_id,
150            registry: Some(registry),
151            callback_registry: None,
152            _phantom: PhantomData,
153        }
154    }
155
156    pub fn new_headless(next_req_id: u64) -> Self {
157        Self {
158            out: Vec::new(),
159            next_req_id,
160            registry: None,
161            callback_registry: None,
162            _phantom: PhantomData,
163        }
164    }
165
166    pub(crate) fn new_runtime(
167        next_req_id: u64,
168        callback_registry: Arc<EffectCallbackRegistry>,
169    ) -> Self {
170        Self {
171            out: Vec::new(),
172            next_req_id,
173            registry: None,
174            callback_registry: Some(callback_registry),
175            _phantom: PhantomData,
176        }
177    }
178
179    pub fn bind<A: Action, H>(&mut self, action: A, handler: H) -> ActionEnvelope
180    where
181        H: IntoHandler<S, A> + Send + Sync + 'static,
182    {
183        let action_id = effect_callback_action_id(A::static_id());
184        if let Some(registry) = &mut self.registry {
185            registry.register_with_id(action_id, handler);
186        } else {
187            let callback_registry = self
188                .callback_registry
189                .as_ref()
190                .unwrap_or_else(|| panic!("Effects::bind requires a runtime or action registry"));
191            let mut registry = ActionRegistry::new();
192            registry.register_with_id(action_id, handler);
193            let mut reducers = registry.into_runtime_reducers();
194            let reducer = reducers
195                .remove(&action_id)
196                .and_then(|mut reducers| reducers.pop())
197                .expect("effect callback reducer must be registered");
198            callback_registry.register(action_id, reducer);
199        }
200        ActionEnvelope {
201            id: action_id,
202            payload: action.encode(),
203        }
204    }
205
206    pub fn add(&mut self, effect: Effect) -> u64 {
207        let req_id = self.next_req_id;
208        self.next_req_id += 1;
209
210        self.out.push(EffectEnvelope {
211            req_id,
212            effect,
213            on_ok: None,
214            on_err: None,
215            service_bindings: None,
216            resource: None,
217        });
218        req_id
219    }
220
221    pub fn capability<C: OperationCapability>(
222        &mut self,
223        capability: CapabilityType<C>,
224        request: C::Request,
225    ) -> EffectBuilder<'_, 'a, S> {
226        let req_id = self.next_req_id;
227        self.next_req_id += 1;
228        let request =
229            serde_json::to_vec(&request).expect("capability request serialization must succeed");
230
231        let index = self.out.len();
232        self.out.push(EffectEnvelope {
233            req_id,
234            effect: Effect::Capability(CapabilityInvocationPayload::Operation(
235                OperationCapabilityInvocation {
236                    capability_name: capability.name.to_string(),
237                    request,
238                },
239            )),
240            on_ok: None,
241            on_err: None,
242            service_bindings: None,
243            resource: None,
244        });
245
246        EffectBuilder {
247            effects: self,
248            index,
249        }
250    }
251
252    /// Starts a typed notification capability request.
253    ///
254    /// Use this from reducers when the app needs the host to request
255    /// notification permission, show or schedule a notification, update a badge,
256    /// or register for push delivery. The returned builder records a capability
257    /// effect; it does not display anything until the reducer has returned and
258    /// the active shell processes queued effects.
259    pub fn notifications(&mut self) -> NotificationEffects<'_, 'a, S> {
260        NotificationEffects { effects: self }
261    }
262
263    /// Starts a typed NFC capability request.
264    ///
265    /// Use this when the app needs the host to read, write, emulate, or cancel
266    /// an NFC session. The helper keeps NFC prompts, tag records, and timeout
267    /// choices in typed request values so reducers do not call platform NFC APIs
268    /// directly.
269    pub fn nfc(&mut self) -> NfcEffects<'_, 'a, S> {
270        NfcEffects { effects: self }
271    }
272
273    /// Starts a typed biometric authentication capability request.
274    ///
275    /// Use this for host-owned local user verification such as fingerprint,
276    /// face, or device credential fallback. The result reports whether the host
277    /// verified the local user; it is not a network identity assertion.
278    pub fn biometrics(&mut self) -> BiometricEffects<'_, 'a, S> {
279        BiometricEffects { effects: self }
280    }
281
282    /// Starts a typed passkey/WebAuthn credential capability request.
283    ///
284    /// Use this for account sign-in, re-authentication, or credential
285    /// registration flows where the server verifies WebAuthn data. This is
286    /// intentionally separate from `biometrics()`: the host may use biometrics
287    /// to unlock a passkey, but the app receives credential data, not raw face
288    /// or fingerprint state.
289    pub fn passkeys(&mut self) -> PasskeyEffects<'_, 'a, S> {
290        PasskeyEffects { effects: self }
291    }
292
293    /// Starts a typed Bluetooth capability request.
294    ///
295    /// Use this for nearby-device workflows such as adapter availability,
296    /// permission requests, scanning, connecting, characteristic reads and
297    /// writes, and advertising. Scans and connections are host-owned operations
298    /// because permission and hardware behavior differ sharply by platform.
299    pub fn bluetooth(&mut self) -> BluetoothEffects<'_, 'a, S> {
300        BluetoothEffects { effects: self }
301    }
302
303    /// Starts a typed barcode scanner capability request.
304    ///
305    /// Use this when the host should run a live scanner session or decode image
306    /// bytes into barcode results. Live scanning normally depends on camera
307    /// permission; image decoding can be tested without camera hardware.
308    pub fn barcode_scanner(&mut self) -> BarcodeScannerEffects<'_, 'a, S> {
309        BarcodeScannerEffects { effects: self }
310    }
311
312    /// Starts a typed camera and flashlight capability request.
313    ///
314    /// Use this for camera availability, permission, still photo capture, and
315    /// torch control. The returned helper emits requests to the shell host so
316    /// the app state does not depend on a particular camera API.
317    pub fn camera(&mut self) -> CameraEffects<'_, 'a, S> {
318        CameraEffects { effects: self }
319    }
320
321    /// Starts a typed clipboard capability request.
322    ///
323    /// Use this for user-visible copy and paste flows. Platforms may restrict
324    /// clipboard access to focused windows, secure browser contexts, or direct
325    /// user gestures, so reducers should handle errors as normal outcomes.
326    pub fn clipboard(&mut self) -> ClipboardEffects<'_, 'a, S> {
327        ClipboardEffects { effects: self }
328    }
329
330    /// Starts a typed geolocation capability request.
331    ///
332    /// Use this when the app needs permission state or a current location. The
333    /// request controls accuracy, timeout, and cache age so the host can choose
334    /// an appropriate platform location call.
335    pub fn geolocation(&mut self) -> GeolocationEffects<'_, 'a, S> {
336        GeolocationEffects { effects: self }
337    }
338
339    /// Starts a typed haptic feedback capability request.
340    ///
341    /// Use this for tactile feedback tied to meaningful interactions such as
342    /// impact, notification, selection, or a bounded pattern. Unsupported hosts
343    /// should return a typed error rather than pretending vibration occurred.
344    pub fn haptics(&mut self) -> HapticEffects<'_, 'a, S> {
345        HapticEffects { effects: self }
346    }
347
348    /// Starts a typed microphone capability request.
349    ///
350    /// Use this for microphone availability, permission, bounded audio capture,
351    /// and cancellation. Captures should be explicit and time-bounded because
352    /// recording is a sensitive host-owned operation.
353    pub fn microphone(&mut self) -> MicrophoneEffects<'_, 'a, S> {
354        MicrophoneEffects { effects: self }
355    }
356
357    /// Starts a typed Wi-Fi capability request.
358    ///
359    /// Use this for adapter availability, permission, scanning, connecting, and
360    /// disconnecting where the platform allows app-level Wi-Fi management.
361    /// Many platforms treat Wi-Fi information as location-sensitive, so reducers
362    /// should handle permission and unsupported errors explicitly.
363    pub fn wifi(&mut self) -> WifiEffects<'_, 'a, S> {
364        WifiEffects { effects: self }
365    }
366
367    /// Starts a typed host volume-control capability request.
368    ///
369    /// Use this for app-approved media, notification, alarm, call, or system
370    /// stream adjustments. Some platforms expose only media-element volume or no
371    /// system-volume control, so callers should treat unsupported errors as
372    /// normal platform outcomes.
373    pub fn volume(&mut self) -> VolumeEffects<'_, 'a, S> {
374        VolumeEffects { effects: self }
375    }
376
377    pub fn app<J: JobSpec>(
378        &mut self,
379        job: JobRef<J>,
380        request: J::Request,
381    ) -> EffectBuilder<'_, 'a, S> {
382        let req_id = self.next_req_id;
383        self.next_req_id += 1;
384        let payload = serde_json::to_vec(&request).expect("job request serialization must succeed");
385        let index = self.out.len();
386        self.out.push(EffectEnvelope {
387            req_id,
388            effect: Effect::Job(JobRequestPayload {
389                job_name: job.name.to_string(),
390                payload,
391            }),
392            on_ok: None,
393            on_err: None,
394            service_bindings: None,
395            resource: None,
396        });
397        EffectBuilder {
398            effects: self,
399            index,
400        }
401    }
402
403    pub fn start_service<Svc: ServiceSpec>(
404        &mut self,
405        slot: ServiceSlot<Svc>,
406        config: Svc::Config,
407    ) -> ServiceStartBuilder<'_, 'a, S> {
408        let req_id = self.next_req_id;
409        self.next_req_id += 1;
410        let index = self.out.len();
411        let config =
412            serde_json::to_vec(&config).expect("service config serialization must succeed");
413        self.out.push(EffectEnvelope {
414            req_id,
415            effect: Effect::StartService(ServiceStartPayload {
416                service_name: slot.ty.name.to_string(),
417                slot_key: slot.slot_key().to_string(),
418                config,
419            }),
420            on_ok: None,
421            on_err: None,
422            service_bindings: Some(ServiceBindings::default()),
423            resource: None,
424        });
425        ServiceStartBuilder {
426            effects: self,
427            index,
428        }
429    }
430
431    pub fn command<Svc: ServiceSpec>(
432        &mut self,
433        slot: ServiceSlot<Svc>,
434        command: Svc::Command,
435    ) -> EffectBuilder<'_, 'a, S> {
436        let req_id = self.next_req_id;
437        self.next_req_id += 1;
438        let index = self.out.len();
439        let payload =
440            serde_json::to_vec(&command).expect("service command serialization must succeed");
441        self.out.push(EffectEnvelope {
442            req_id,
443            effect: Effect::ServiceCommand(ServiceCommandPayload {
444                service_name: slot.ty.name.to_string(),
445                slot_key: slot.slot_key().to_string(),
446                payload,
447            }),
448            on_ok: None,
449            on_err: None,
450            service_bindings: None,
451            resource: None,
452        });
453        EffectBuilder {
454            effects: self,
455            index,
456        }
457    }
458
459    pub fn stop_service<Svc: ServiceSpec>(
460        &mut self,
461        slot: ServiceSlot<Svc>,
462    ) -> EffectBuilder<'_, 'a, S> {
463        let req_id = self.next_req_id;
464        self.next_req_id += 1;
465        let index = self.out.len();
466        self.out.push(EffectEnvelope {
467            req_id,
468            effect: Effect::StopService(ServiceStopPayload {
469                service_name: slot.ty.name.to_string(),
470                slot_key: slot.slot_key().to_string(),
471            }),
472            on_ok: None,
473            on_err: None,
474            service_bindings: None,
475            resource: None,
476        });
477        EffectBuilder {
478            effects: self,
479            index,
480        }
481    }
482
483    pub fn cancel(&mut self, req_id: u64) {
484        self.add(Effect::Runtime(RuntimeEffect::Cancel { req_id }));
485    }
486
487    pub fn release_resource(&mut self, resource_id: u64) {
488        self.add(Effect::Runtime(RuntimeEffect::ReleaseResource {
489            resource_id,
490        }));
491    }
492
493    /// Reveals a widget inside a scroll container after the next layout pass.
494    ///
495    /// This is safe to emit from reducers because the runtime resolves widget
496    /// rectangles later, after layout has produced stable geometry.
497    pub fn scroll_into_view(&mut self, request: ScrollIntoViewRequest) -> u64 {
498        self.add(Effect::Runtime(RuntimeEffect::ScrollIntoView(request)))
499    }
500
501    /// Alias for [`Effects::scroll_into_view`] when the caller cares about
502    /// visibility rather than a specific scroll operation.
503    pub fn ensure_visible(&mut self, request: ScrollIntoViewRequest) -> u64 {
504        self.scroll_into_view(request)
505    }
506}
507
508fn effect_callback_action_id(action_id: ActionId) -> ActionId {
509    let sequence = NEXT_EFFECT_CALLBACK_ID.fetch_add(1, Ordering::Relaxed);
510    ActionId::from_name(&format!(
511        "fission::effect_callback::{:032x}::{sequence}",
512        action_id.as_u128()
513    ))
514}
515
516/// Convenience builder for the standard notification host capabilities.
517pub struct NotificationEffects<'a, 'b, S: GlobalState> {
518    effects: &'a mut Effects<'b, S>,
519}
520
521impl<'a, 'b, S: GlobalState> NotificationEffects<'a, 'b, S> {
522    /// Requests notification permission from the active host.
523    ///
524    /// `request` declares which notification features the app wants, such as
525    /// alerts, badges, sounds, or provisional delivery. The returned
526    /// `EffectBuilder` should normally bind success and error actions so the
527    /// reducer can update state after the user responds to the platform prompt.
528    pub fn request_permission(
529        self,
530        request: NotificationPermissionRequest,
531    ) -> EffectBuilder<'a, 'b, S> {
532        self.effects
533            .capability(REQUEST_NOTIFICATION_PERMISSION, request)
534    }
535
536    /// Queries the host's current notification settings without showing a prompt.
537    ///
538    /// Use this before rendering notification-dependent controls or when a
539    /// settings screen needs to explain why delivery is unavailable. The success
540    /// action receives `NotificationSettings`.
541    pub fn settings(self) -> EffectBuilder<'a, 'b, S> {
542        self.effects.capability(GET_NOTIFICATION_SETTINGS, ())
543    }
544
545    /// Shows an immediate local notification through the host.
546    ///
547    /// `request` supplies the stable notification id, visible title/body, badge,
548    /// sound policy, optional deep link, and action buttons. Use `schedule`
549    /// instead when delivery should happen at a future time.
550    pub fn show(self, request: NotificationRequest) -> EffectBuilder<'a, 'b, S> {
551        self.effects.capability(SHOW_NOTIFICATION, request)
552    }
553
554    /// Schedules a local notification for future delivery.
555    ///
556    /// The `schedule` field on `request` controls the delivery time. Hosts may
557    /// reject schedules they cannot persist, cannot deliver in the background, or
558    /// cannot map to the platform notification model.
559    pub fn schedule(self, request: NotificationRequest) -> EffectBuilder<'a, 'b, S> {
560        self.effects.capability(SCHEDULE_NOTIFICATION, request)
561    }
562
563    /// Cancels one pending or displayed notification by id.
564    ///
565    /// Use the same `NotificationId` that was used for `show` or `schedule`. A
566    /// host may treat cancelling an unknown id as success if the desired final
567    /// state is already true.
568    pub fn cancel(self, request: CancelNotificationRequest) -> EffectBuilder<'a, 'b, S> {
569        self.effects.capability(CANCEL_NOTIFICATION, request)
570    }
571
572    /// Cancels all notifications owned by this app where the host supports it.
573    ///
574    /// Use this for sign-out, workspace switching, or clearing a notification
575    /// center state that no longer matches app state. Hosts that cannot bulk
576    /// cancel should return `NotificationError`.
577    pub fn cancel_all(self) -> EffectBuilder<'a, 'b, S> {
578        self.effects.capability(CANCEL_ALL_NOTIFICATIONS, ())
579    }
580
581    /// Sets or clears the app badge count.
582    ///
583    /// `request.count = Some(n)` asks the host to show a badge count.
584    /// `request.count = None` clears the badge. Badge support varies by desktop
585    /// shell, launcher, browser, and mobile platform.
586    pub fn set_badge_count(self, request: SetBadgeCountRequest) -> EffectBuilder<'a, 'b, S> {
587        self.effects.capability(SET_BADGE_COUNT, request)
588    }
589
590    /// Registers the app for remote or push notifications.
591    ///
592    /// `request` carries provider-specific public registration inputs such as a
593    /// web push application-server key, Android sender id, or requested topics.
594    /// Secrets and store credentials belong in host configuration, not in app
595    /// state.
596    pub fn register_push(self, request: PushRegistrationRequest) -> EffectBuilder<'a, 'b, S> {
597        self.effects
598            .capability(REGISTER_PUSH_NOTIFICATIONS, request)
599    }
600
601    /// Unregisters the app from remote or push notification delivery.
602    ///
603    /// Use this during sign-out, account removal, or when a user disables remote
604    /// notifications. The host should invalidate or delete its platform token
605    /// where the provider supports that operation.
606    pub fn unregister_push(self) -> EffectBuilder<'a, 'b, S> {
607        self.effects.capability(UNREGISTER_PUSH_NOTIFICATIONS, ())
608    }
609}
610
611/// Convenience builder for standard NFC host capabilities.
612pub struct NfcEffects<'a, 'b, S: GlobalState> {
613    effects: &'a mut Effects<'b, S>,
614}
615
616impl<'a, 'b, S: GlobalState> NfcEffects<'a, 'b, S> {
617    /// Queries whether NFC is supported, enabled, and which NFC modes are available.
618    ///
619    /// Use this before showing scan/write controls so the UI can distinguish a
620    /// missing NFC chip from a disabled adapter or unsupported operation.
621    pub fn availability(self) -> EffectBuilder<'a, 'b, S> {
622        self.effects.capability(GET_NFC_AVAILABILITY, ())
623    }
624
625    /// Starts a one-shot NFC scan session.
626    ///
627    /// `request` declares allowed technologies, optional user-facing prompt text,
628    /// timeout, and whether multiple records should be collected. The success
629    /// action receives an `NfcTag` when the host reads a compatible tag.
630    pub fn scan_tag(self, request: NfcScanRequest) -> EffectBuilder<'a, 'b, S> {
631        self.effects.capability(SCAN_NFC_TAG, request)
632    }
633
634    /// Starts an NFC tag write session.
635    ///
636    /// `request.records` contains the portable NDEF-like records to write. Hosts
637    /// may require the user to tap a writable tag after the operation starts and
638    /// may reject read-only or incompatible tags.
639    pub fn write_tag(self, request: NfcWriteRequest) -> EffectBuilder<'a, 'b, S> {
640        self.effects.capability(WRITE_NFC_TAG, request)
641    }
642
643    /// Requests host NFC card emulation for the supplied records.
644    ///
645    /// Use this only on platforms and devices that support card emulation for
646    /// the product scenario. Many hosts support scanning but not emulation.
647    pub fn emulate_tag(self, request: NfcEmulationRequest) -> EffectBuilder<'a, 'b, S> {
648        self.effects.capability(EMULATE_NFC_TAG, request)
649    }
650
651    /// Cancels the active NFC session, if one is running.
652    ///
653    /// Use this when the user dismisses the screen that started scanning, writing,
654    /// or emulation. Hosts may return success when no session is active.
655    pub fn cancel_session(self) -> EffectBuilder<'a, 'b, S> {
656        self.effects.capability(CANCEL_NFC_SESSION, ())
657    }
658}
659
660/// Convenience builder for standard biometric host capabilities.
661pub struct BiometricEffects<'a, 'b, S: GlobalState> {
662    effects: &'a mut Effects<'b, S>,
663}
664
665impl<'a, 'b, S: GlobalState> BiometricEffects<'a, 'b, S> {
666    /// Queries local biometric support and enrollment state.
667    ///
668    /// Use this before presenting a biometric-only path. The result tells the app
669    /// whether the host supports biometric verification, whether credentials are
670    /// enrolled, which modalities may be available, and whether device credential
671    /// fallback is possible.
672    pub fn availability(self) -> EffectBuilder<'a, 'b, S> {
673        self.effects.capability(GET_BIOMETRIC_AVAILABILITY, ())
674    }
675
676    /// Asks the host to authenticate the current local user.
677    ///
678    /// `request.reason` should explain why verification is needed before the
679    /// platform prompt appears. The success action receives
680    /// `BiometricAuthenticateResult`, which reports the modality and whether a
681    /// device credential fallback was used.
682    pub fn authenticate(self, request: BiometricAuthenticateRequest) -> EffectBuilder<'a, 'b, S> {
683        self.effects.capability(AUTHENTICATE_BIOMETRIC, request)
684    }
685
686    /// Cancels an active biometric authentication prompt where the host permits it.
687    ///
688    /// Use this when the screen that requested verification is closed or the app
689    /// changes state before the user responds. Some platform prompts cannot be
690    /// cancelled programmatically after display.
691    pub fn cancel_authentication(self) -> EffectBuilder<'a, 'b, S> {
692        self.effects.capability(CANCEL_BIOMETRIC_AUTHENTICATION, ())
693    }
694}
695
696/// Convenience builder for standard passkey/WebAuthn host capabilities.
697pub struct PasskeyEffects<'a, 'b, S: GlobalState> {
698    effects: &'a mut Effects<'b, S>,
699}
700
701impl<'a, 'b, S: GlobalState> PasskeyEffects<'a, 'b, S> {
702    /// Queries passkey support for the active host and origin.
703    ///
704    /// Use this before showing passkey-specific registration or sign-in controls.
705    /// The result tells the app whether the host supports passkeys, whether the
706    /// current context is secure enough for credential APIs, and whether platform
707    /// or conditional UI authenticators may be available.
708    pub fn availability(self) -> EffectBuilder<'a, 'b, S> {
709        self.effects.capability(GET_PASSKEY_AVAILABILITY, ())
710    }
711
712    /// Requests creation of a new passkey credential.
713    ///
714    /// `request.challenge` must come from the relying-party server and must be
715    /// verified by that server when the success action receives
716    /// `PasskeyRegistrationResult`. Do not generate production challenges in the
717    /// UI reducer or trust registration data until the backend verifies it.
718    pub fn register(self, request: PasskeyRegistrationRequest) -> EffectBuilder<'a, 'b, S> {
719        self.effects.capability(REGISTER_PASSKEY, request)
720    }
721
722    /// Requests authentication with an existing passkey credential.
723    ///
724    /// `request.challenge` must come from the server, and the returned
725    /// `PasskeyAuthenticationResult` must be verified by the server before the
726    /// app treats the user as signed in. The host only gathers credential data.
727    pub fn authenticate(self, request: PasskeyAuthenticationRequest) -> EffectBuilder<'a, 'b, S> {
728        self.effects.capability(AUTHENTICATE_PASSKEY, request)
729    }
730
731    /// Cancels an active passkey prompt where the host permits cancellation.
732    ///
733    /// Use this when the sign-in or registration screen disappears before the
734    /// host credential picker completes. Some browser or operating-system
735    /// prompts cannot be cancelled once shown.
736    pub fn cancel(self) -> EffectBuilder<'a, 'b, S> {
737        self.effects.capability(CANCEL_PASSKEY_OPERATION, ())
738    }
739}
740
741/// Convenience builder for standard Bluetooth host capabilities.
742pub struct BluetoothEffects<'a, 'b, S: GlobalState> {
743    effects: &'a mut Effects<'b, S>,
744}
745
746impl<'a, 'b, S: GlobalState> BluetoothEffects<'a, 'b, S> {
747    /// Queries Bluetooth adapter, permission, and mode availability.
748    ///
749    /// Use this before showing scan, connect, or advertise controls. The result
750    /// lets the UI distinguish missing hardware, disabled Bluetooth, permission
751    /// denial, and hosts that support only classic or Low Energy Bluetooth.
752    pub fn availability(self) -> EffectBuilder<'a, 'b, S> {
753        self.effects.capability(GET_BLUETOOTH_AVAILABILITY, ())
754    }
755
756    /// Requests Bluetooth or nearby-device permission from the host.
757    ///
758    /// `request.reason` should explain the product feature that needs nearby
759    /// devices. Hosts map the request to the platform permission model, which may
760    /// include Bluetooth, location, or nearby-device prompts depending on target.
761    pub fn request_permission(
762        self,
763        request: BluetoothPermissionRequest,
764    ) -> EffectBuilder<'a, 'b, S> {
765        self.effects
766            .capability(REQUEST_BLUETOOTH_PERMISSION, request)
767    }
768
769    /// Scans for Bluetooth devices matching the request filters.
770    ///
771    /// `request.service_uuids` narrows discovery to product-relevant services.
772    /// `timeout_ms` should be set for user-driven scans so the host does not keep
773    /// nearby-device discovery running indefinitely.
774    pub fn scan_devices(self, request: BluetoothScanRequest) -> EffectBuilder<'a, 'b, S> {
775        self.effects.capability(SCAN_BLUETOOTH_DEVICES, request)
776    }
777
778    /// Connects to a discovered or previously known Bluetooth device.
779    ///
780    /// `request.device_id` must come from a trusted host result or stored pairing
781    /// flow. The success action receives a `BluetoothConnection` whose
782    /// `connection_id` is used for later read, write, and disconnect requests.
783    pub fn connect_device(self, request: BluetoothConnectRequest) -> EffectBuilder<'a, 'b, S> {
784        self.effects.capability(CONNECT_BLUETOOTH_DEVICE, request)
785    }
786
787    /// Disconnects a previously opened Bluetooth connection.
788    ///
789    /// `request.connection_id` should be the id returned by `connect_device`.
790    /// Use this when the user leaves the device workflow or when the app no
791    /// longer needs the peripheral.
792    pub fn disconnect_device(
793        self,
794        request: BluetoothDisconnectRequest,
795    ) -> EffectBuilder<'a, 'b, S> {
796        self.effects
797            .capability(DISCONNECT_BLUETOOTH_DEVICE, request)
798    }
799
800    /// Reads one Bluetooth characteristic from an active connection.
801    ///
802    /// `request` names the connection, service UUID, and characteristic UUID.
803    /// Hosts should return `BluetoothError` when the connection is gone or the
804    /// characteristic is unavailable.
805    pub fn read_characteristic(self, request: BluetoothReadRequest) -> EffectBuilder<'a, 'b, S> {
806        self.effects
807            .capability(READ_BLUETOOTH_CHARACTERISTIC, request)
808    }
809
810    /// Writes bytes to one Bluetooth characteristic.
811    ///
812    /// `request.with_response` lets the app choose between acknowledged and
813    /// unacknowledged writes where the platform supports both. Reducers should
814    /// still handle connection loss and permission errors as normal outcomes.
815    pub fn write_characteristic(self, request: BluetoothWriteRequest) -> EffectBuilder<'a, 'b, S> {
816        self.effects
817            .capability(WRITE_BLUETOOTH_CHARACTERISTIC, request)
818    }
819
820    /// Starts Bluetooth advertising for hosts that allow apps to advertise.
821    ///
822    /// `request` supplies the service UUID, optional service data, display name,
823    /// and timeout. Mobile and browser platforms often restrict advertising more
824    /// heavily than scanning or connecting.
825    pub fn start_advertising(self, request: BluetoothAdvertiseRequest) -> EffectBuilder<'a, 'b, S> {
826        self.effects
827            .capability(START_BLUETOOTH_ADVERTISING, request)
828    }
829
830    /// Stops a Bluetooth advertising session.
831    ///
832    /// `request.advertisement_id` should be the id returned by
833    /// `start_advertising`. Hosts may also stop advertisements automatically when
834    /// their timeout expires or the app moves to a background state.
835    pub fn stop_advertising(
836        self,
837        request: BluetoothStopAdvertiseRequest,
838    ) -> EffectBuilder<'a, 'b, S> {
839        self.effects.capability(STOP_BLUETOOTH_ADVERTISING, request)
840    }
841}
842
843/// Convenience builder for standard barcode scanner host capabilities.
844pub struct BarcodeScannerEffects<'a, 'b, S: GlobalState> {
845    effects: &'a mut Effects<'b, S>,
846}
847
848impl<'a, 'b, S: GlobalState> BarcodeScannerEffects<'a, 'b, S> {
849    /// Starts a live barcode scanning session.
850    ///
851    /// `request.formats` should list only formats the product accepts. The host
852    /// may open a camera UI, display `prompt`, and return one or more decoded
853    /// barcode values depending on `allow_multiple`.
854    pub fn scan(self, request: BarcodeScanRequest) -> EffectBuilder<'a, 'b, S> {
855        self.effects.capability(SCAN_BARCODE, request)
856    }
857
858    /// Decodes barcode data from an image stream supplied by the app.
859    ///
860    /// Use this when the image already exists, such as a file import or camera
861    /// frame captured elsewhere. The host should not request camera permission
862    /// for this operation unless its decoder specifically requires it.
863    pub fn decode_image(self, request: BarcodeImageDecodeRequest) -> EffectBuilder<'a, 'b, S> {
864        self.effects.capability(DECODE_BARCODE_IMAGE, request)
865    }
866
867    /// Cancels the active live barcode scanning session.
868    ///
869    /// Use this when the user leaves the scanning screen or chooses another input
870    /// path. Hosts may treat cancellation of a non-running session as success.
871    pub fn cancel_scan(self) -> EffectBuilder<'a, 'b, S> {
872        self.effects.capability(CANCEL_BARCODE_SCAN, ())
873    }
874}
875
876/// Convenience builder for standard camera host capabilities.
877pub struct CameraEffects<'a, 'b, S: GlobalState> {
878    effects: &'a mut Effects<'b, S>,
879}
880
881impl<'a, 'b, S: GlobalState> CameraEffects<'a, 'b, S> {
882    /// Queries camera permission and available camera devices.
883    ///
884    /// Use this before showing camera-specific controls. The result contains the
885    /// current permission state and host-visible devices, including facing
886    /// direction and flashlight availability where known.
887    pub fn availability(self) -> EffectBuilder<'a, 'b, S> {
888        self.effects.capability(GET_CAMERA_AVAILABILITY, ())
889    }
890
891    /// Requests camera permission from the host.
892    ///
893    /// `request.reason` can carry product-facing context for hosts that support a
894    /// pre-prompt or custom rationale. The success action receives the resulting
895    /// `CameraPermission` state.
896    pub fn request_permission(self, request: CameraPermissionRequest) -> EffectBuilder<'a, 'b, S> {
897        self.effects.capability(REQUEST_CAMERA_PERMISSION, request)
898    }
899
900    /// Captures a still photo through the selected host camera.
901    ///
902    /// `request` chooses camera id or facing direction, optional resolution, image
903    /// format, flash behavior, and quality. The success action receives image
904    /// stream handle plus dimensions, byte length, and content type.
905    pub fn capture_photo(self, request: CameraCaptureRequest) -> EffectBuilder<'a, 'b, S> {
906        self.effects.capability(CAPTURE_PHOTO, request)
907    }
908
909    /// Enables, disables, or adjusts the camera flashlight where supported.
910    ///
911    /// `request.camera_id` selects the device, `enabled` chooses the desired
912    /// state, and `intensity` optionally requests a platform-specific brightness
913    /// level from 0 to 100. Many desktop cameras have no torch.
914    pub fn set_flashlight(self, request: CameraFlashlightRequest) -> EffectBuilder<'a, 'b, S> {
915        self.effects.capability(SET_CAMERA_FLASHLIGHT, request)
916    }
917
918    /// Cancels an active camera capture session.
919    ///
920    /// Use this when the user dismisses the camera flow before a photo is
921    /// returned. Hosts may return success when there is no active capture.
922    pub fn cancel_capture(self) -> EffectBuilder<'a, 'b, S> {
923        self.effects.capability(CANCEL_CAMERA_CAPTURE, ())
924    }
925}
926
927/// Convenience builder for standard clipboard host capabilities.
928pub struct ClipboardEffects<'a, 'b, S: GlobalState> {
929    effects: &'a mut Effects<'b, S>,
930}
931
932impl<'a, 'b, S: GlobalState> ClipboardEffects<'a, 'b, S> {
933    /// Reads text from the host clipboard.
934    ///
935    /// Use this in response to an explicit paste action. The success action
936    /// receives `ClipboardText` with `None` when there is no readable text.
937    pub fn read_text(self) -> EffectBuilder<'a, 'b, S> {
938        self.effects.capability(READ_CLIPBOARD_TEXT, ())
939    }
940
941    /// Writes plain text to the host clipboard.
942    ///
943    /// `request.text` should be the exact text the user asked to copy. Some hosts
944    /// may require focus or a user gesture before accepting the write.
945    pub fn write_text(self, request: ClipboardWriteTextRequest) -> EffectBuilder<'a, 'b, S> {
946        self.effects.capability(WRITE_CLIPBOARD_TEXT, request)
947    }
948
949    /// Reads typed clipboard content from the host.
950    ///
951    /// Use this when the product can accept richer content than plain text. The
952    /// success action receives zero or more `ClipboardItem` values with content
953    /// types and bytes.
954    pub fn read_content(self) -> EffectBuilder<'a, 'b, S> {
955        self.effects.capability(READ_CLIPBOARD_CONTENT, ())
956    }
957
958    /// Writes typed content items to the host clipboard.
959    ///
960    /// `request.items` should list content types the target host can expose.
961    /// Include a `text/plain` item when possible so paste targets have a portable
962    /// fallback.
963    pub fn write_content(self, request: ClipboardContent) -> EffectBuilder<'a, 'b, S> {
964        self.effects.capability(WRITE_CLIPBOARD_CONTENT, request)
965    }
966
967    /// Clears app-visible clipboard content where the host supports it.
968    ///
969    /// Use this for explicit privacy actions such as Clear copied password. Some
970    /// platforms may not allow apps to clear global clipboard state.
971    pub fn clear(self) -> EffectBuilder<'a, 'b, S> {
972        self.effects.capability(CLEAR_CLIPBOARD, ())
973    }
974}
975
976/// Convenience builder for standard geolocation host capabilities.
977pub struct GeolocationEffects<'a, 'b, S: GlobalState> {
978    effects: &'a mut Effects<'b, S>,
979}
980
981impl<'a, 'b, S: GlobalState> GeolocationEffects<'a, 'b, S> {
982    /// Reads the current geolocation permission state without showing a prompt.
983    ///
984    /// Use this to decide whether a screen should show a request button, an
985    /// explanation, or a platform-settings hint. The result is host-reported and
986    /// may change outside the app.
987    pub fn permission(self) -> EffectBuilder<'a, 'b, S> {
988        self.effects.capability(GET_GEOLOCATION_PERMISSION, ())
989    }
990
991    /// Requests geolocation permission from the host.
992    ///
993    /// `request.precise` asks for precise coordinates when the platform exposes a
994    /// precise/approximate distinction. `request.background` should only be set
995    /// for product flows that genuinely need background location and have matching
996    /// platform configuration.
997    pub fn request_permission(
998        self,
999        request: GeolocationPermissionRequest,
1000    ) -> EffectBuilder<'a, 'b, S> {
1001        self.effects
1002            .capability(REQUEST_GEOLOCATION_PERMISSION, request)
1003    }
1004
1005    /// Requests the current location from the host.
1006    ///
1007    /// `request.high_accuracy`, `timeout_ms`, and `maximum_age_ms` let the app
1008    /// trade precision, speed, power use, and cached values. The success action
1009    /// receives latitude, longitude, accuracy, and optional motion metadata.
1010    pub fn current_position(self, request: GeolocationPositionRequest) -> EffectBuilder<'a, 'b, S> {
1011        self.effects.capability(GET_CURRENT_POSITION, request)
1012    }
1013}
1014
1015/// Convenience builder for standard haptic host capabilities.
1016pub struct HapticEffects<'a, 'b, S: GlobalState> {
1017    effects: &'a mut Effects<'b, S>,
1018}
1019
1020impl<'a, 'b, S: GlobalState> HapticEffects<'a, 'b, S> {
1021    /// Plays impact-style haptic feedback.
1022    ///
1023    /// Use this for physical-feeling interactions such as completing a drag,
1024    /// snapping to a position, or confirming a strong action. The `style` field
1025    /// tells the host how heavy the feedback should feel.
1026    pub fn impact(self, request: HapticImpactRequest) -> EffectBuilder<'a, 'b, S> {
1027        self.effects.capability(HAPTIC_IMPACT, request)
1028    }
1029
1030    /// Plays notification-style haptic feedback.
1031    ///
1032    /// Use this to reinforce success, warning, or error states when tactile
1033    /// feedback improves understanding. It should not replace visible or spoken
1034    /// feedback for accessibility.
1035    pub fn notification(self, request: HapticNotificationRequest) -> EffectBuilder<'a, 'b, S> {
1036        self.effects.capability(HAPTIC_NOTIFICATION, request)
1037    }
1038
1039    /// Plays selection-change haptic feedback.
1040    ///
1041    /// Use this for picker movement, segmented-control changes, or other repeated
1042    /// selection adjustments where a light tick helps the user track movement.
1043    pub fn selection(self) -> EffectBuilder<'a, 'b, S> {
1044        self.effects.capability(HAPTIC_SELECTION, ())
1045    }
1046
1047    /// Plays a bounded custom haptic pattern.
1048    ///
1049    /// `request.steps` contains duration and intensity values. Keep patterns
1050    /// short and meaningful; hosts may reject long, empty, or unsupported
1051    /// patterns.
1052    pub fn pattern(self, request: HapticPatternRequest) -> EffectBuilder<'a, 'b, S> {
1053        self.effects.capability(HAPTIC_PATTERN, request)
1054    }
1055}
1056
1057/// Convenience builder for standard microphone host capabilities.
1058pub struct MicrophoneEffects<'a, 'b, S: GlobalState> {
1059    effects: &'a mut Effects<'b, S>,
1060}
1061
1062impl<'a, 'b, S: GlobalState> MicrophoneEffects<'a, 'b, S> {
1063    /// Queries microphone permission and available input devices.
1064    ///
1065    /// Use this before showing recording controls. The result tells the app
1066    /// whether microphone permission is granted and which host input devices are
1067    /// visible.
1068    pub fn availability(self) -> EffectBuilder<'a, 'b, S> {
1069        self.effects.capability(GET_MICROPHONE_AVAILABILITY, ())
1070    }
1071
1072    /// Requests microphone permission from the host.
1073    ///
1074    /// `request.reason` can be used by hosts that support a product-specific
1075    /// rationale before the platform prompt. The success action receives the
1076    /// resulting `MicrophonePermission` state.
1077    pub fn request_permission(
1078        self,
1079        request: MicrophonePermissionRequest,
1080    ) -> EffectBuilder<'a, 'b, S> {
1081        self.effects
1082            .capability(REQUEST_MICROPHONE_PERMISSION, request)
1083    }
1084
1085    /// Captures bounded audio from the selected microphone.
1086    ///
1087    /// `request.duration_ms` must define the intended capture length. Optional
1088    /// sample rate, channel count, and sample format let the host choose the
1089    /// closest supported recording configuration. The success action receives a
1090    /// stream handle plus recording metadata.
1091    pub fn capture_audio(self, request: MicrophoneCaptureRequest) -> EffectBuilder<'a, 'b, S> {
1092        self.effects.capability(CAPTURE_MICROPHONE_AUDIO, request)
1093    }
1094
1095    /// Cancels an active microphone capture session.
1096    ///
1097    /// Use this when the user stops recording, closes the screen, or chooses a
1098    /// different input path before the bounded capture completes.
1099    pub fn cancel_capture(self) -> EffectBuilder<'a, 'b, S> {
1100        self.effects.capability(CANCEL_MICROPHONE_CAPTURE, ())
1101    }
1102}
1103
1104/// Convenience builder for standard Wi-Fi host capabilities.
1105pub struct WifiEffects<'a, 'b, S: GlobalState> {
1106    effects: &'a mut Effects<'b, S>,
1107}
1108
1109impl<'a, 'b, S: GlobalState> WifiEffects<'a, 'b, S> {
1110    /// Queries current Wi-Fi adapter and connection availability.
1111    ///
1112    /// Use this before showing scan or connect controls. The result can include
1113    /// whether the adapter is enabled and which network, if any, is connected.
1114    pub fn availability(self) -> EffectBuilder<'a, 'b, S> {
1115        self.effects.capability(GET_WIFI_AVAILABILITY, ())
1116    }
1117
1118    /// Requests Wi-Fi or nearby-network permission from the host.
1119    ///
1120    /// `request.reason` should describe the feature that needs network discovery
1121    /// or management. Hosts may map this to Wi-Fi, nearby-device, or location
1122    /// permission prompts depending on platform policy.
1123    pub fn request_permission(self, request: WifiPermissionRequest) -> EffectBuilder<'a, 'b, S> {
1124        self.effects.capability(REQUEST_WIFI_PERMISSION, request)
1125    }
1126
1127    /// Scans for nearby Wi-Fi networks where the host permits scanning.
1128    ///
1129    /// `request.ssid_prefix` narrows results for device-setup flows,
1130    /// `include_hidden` asks the host to include hidden networks when possible,
1131    /// and `timeout_ms` bounds the scan.
1132    pub fn scan_networks(self, request: WifiScanRequest) -> EffectBuilder<'a, 'b, S> {
1133        self.effects.capability(SCAN_WIFI_NETWORKS, request)
1134    }
1135
1136    /// Requests connection to one Wi-Fi network.
1137    ///
1138    /// `request` carries SSID, optional passphrase, security type, and hidden
1139    /// network flag. Hosts may reject connections that require user confirmation,
1140    /// saved network profiles, entitlements, or administrator privileges.
1141    pub fn connect_network(self, request: WifiConnectRequest) -> EffectBuilder<'a, 'b, S> {
1142        self.effects.capability(CONNECT_WIFI_NETWORK, request)
1143    }
1144
1145    /// Requests disconnection from a Wi-Fi network.
1146    ///
1147    /// `request.ssid` can limit the operation to a specific network when the host
1148    /// supports that distinction. Some platforms do not allow apps to disconnect
1149    /// global network state.
1150    pub fn disconnect_network(self, request: WifiDisconnectRequest) -> EffectBuilder<'a, 'b, S> {
1151        self.effects.capability(DISCONNECT_WIFI_NETWORK, request)
1152    }
1153}
1154
1155/// Convenience builder for standard volume-control host capabilities.
1156pub struct VolumeEffects<'a, 'b, S: GlobalState> {
1157    effects: &'a mut Effects<'b, S>,
1158}
1159
1160impl<'a, 'b, S: GlobalState> VolumeEffects<'a, 'b, S> {
1161    /// Reads the current level for one host volume stream.
1162    ///
1163    /// `stream` identifies the logical audio stream the app cares about. Hosts
1164    /// map that stream to the closest platform mixer or media channel and return
1165    /// a `VolumeLevel` with level and mute state.
1166    pub fn get_level(self, stream: VolumeStream) -> EffectBuilder<'a, 'b, S> {
1167        self.effects.capability(GET_VOLUME_LEVEL, stream)
1168    }
1169
1170    /// Sets the level and optional mute state for one host volume stream.
1171    ///
1172    /// `request.level` is a percentage-like value from 0 to 100. Hosts should
1173    /// clamp or reject values they cannot represent and return a typed error when
1174    /// the platform does not expose system volume control.
1175    pub fn set_level(self, request: VolumeSetRequest) -> EffectBuilder<'a, 'b, S> {
1176        self.effects.capability(SET_VOLUME_LEVEL, request)
1177    }
1178
1179    /// Adjusts a host volume stream relative to its current level.
1180    ///
1181    /// `request.direction` chooses increase, decrease, or toggle mute, and
1182    /// `request.step` controls the requested amount. Use this for keyboard-like
1183    /// or remote-control volume actions.
1184    pub fn adjust_level(self, request: VolumeAdjustRequest) -> EffectBuilder<'a, 'b, S> {
1185        self.effects.capability(ADJUST_VOLUME_LEVEL, request)
1186    }
1187}
1188
1189/// Fluent builder returned by [`Effects::capability`], [`Effects::app`], and
1190/// related effect constructors.
1191///
1192/// Attach `on_ok` and `on_err` callback envelopes before the builder is dropped.
1193///
1194/// # Example
1195///
1196/// ```rust,ignore
1197/// ctx.effects.capability(MY_CAPABILITY, request)
1198///     .on_ok(ok_envelope)
1199///     .on_err(err_envelope)
1200///     .dispatch(); // optional -- dropping also finalises
1201/// ```
1202pub struct EffectBuilder<'a, 'b, S: GlobalState> {
1203    effects: &'a mut Effects<'b, S>,
1204    index: usize,
1205}
1206
1207impl<'a, 'b, S: GlobalState> EffectBuilder<'a, 'b, S> {
1208    pub fn on_ok(self, action: ActionEnvelope) -> Self {
1209        self.effects.out[self.index].on_ok = Some(action);
1210        self
1211    }
1212
1213    pub fn on_err(self, action: ActionEnvelope) -> Self {
1214        self.effects.out[self.index].on_err = Some(action);
1215        self
1216    }
1217
1218    pub fn dispatch(self) {
1219        // Drop
1220    }
1221}
1222
1223pub struct ServiceStartBuilder<'a, 'b, S: GlobalState> {
1224    effects: &'a mut Effects<'b, S>,
1225    index: usize,
1226}
1227
1228impl<'a, 'b, S: GlobalState> ServiceStartBuilder<'a, 'b, S> {
1229    pub fn on_started(self, action: ActionEnvelope) -> Self {
1230        if let Some(bindings) = self.effects.out[self.index].service_bindings.as_mut() {
1231            bindings.on_started = Some(action);
1232        }
1233        self
1234    }
1235
1236    pub fn on_start_failed(self, action: ActionEnvelope) -> Self {
1237        if let Some(bindings) = self.effects.out[self.index].service_bindings.as_mut() {
1238            bindings.on_start_failed = Some(action);
1239        }
1240        self
1241    }
1242
1243    pub fn on_event(self, action: ActionEnvelope) -> Self {
1244        if let Some(bindings) = self.effects.out[self.index].service_bindings.as_mut() {
1245            bindings.on_event = Some(action);
1246        }
1247        self
1248    }
1249
1250    pub fn on_stopped(self, action: ActionEnvelope) -> Self {
1251        if let Some(bindings) = self.effects.out[self.index].service_bindings.as_mut() {
1252            bindings.on_stopped = Some(action);
1253        }
1254        self
1255    }
1256
1257    pub fn on_command_ok(self, action: ActionEnvelope) -> Self {
1258        if let Some(bindings) = self.effects.out[self.index].service_bindings.as_mut() {
1259            bindings.on_command_ok = Some(action);
1260        }
1261        self
1262    }
1263
1264    pub fn on_command_err(self, action: ActionEnvelope) -> Self {
1265        if let Some(bindings) = self.effects.out[self.index].service_bindings.as_mut() {
1266            bindings.on_command_err = Some(action);
1267        }
1268        self
1269    }
1270
1271    pub fn dispatch(self) {}
1272}