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