Skip to main content

storekit/
async_api.rs

1//! Async API for `StoreKit` — Tier 1 Future wrappers.
2//!
3//! This module requires the **`async`** Cargo feature:
4//! ```toml
5//! storekit-rs = { version = "0.3", features = ["async"] }
6//! ```
7//!
8//! Every public type is an executor-agnostic [`Future`] backed by a
9//! `doom_fish_utils` completion handler.  The futures work with any async
10//! runtime (`tokio`, `async-std`, `smol`, `pollster`, …).
11//!
12//! ## Available types
13//!
14//! | Type | Description |
15//! |------|-------------|
16//! | [`AsyncProducts`] | Fetch products by identifier |
17//! | [`AsyncPurchase`] | Purchase a product |
18//! | [`AsyncAppStore`] | Request review / manage subscriptions |
19//! | [`AsyncAppTransaction`] | Fetch the app transaction |
20//! | [`AsyncStorefront`] | Fetch the current storefront |
21//!
22//! ## `AsyncSequence` APIs — deferred to Tier 2
23//!
24//! The following `StoreKit` APIs expose `AsyncSequence` (multi-fire streams).
25//! They are intentionally **not** included here; they will be wrapped as
26//! `Stream` types in the Tier 2 rollout:
27//!
28//! - `Transaction.updates` — use [`crate::transaction::TransactionStream`] in the meantime
29//! - `Transaction.currentEntitlements` — use [`crate::transaction::TransactionStream`]
30//! - `Transaction.unfinished` — use [`crate::transaction::TransactionStream`]
31//!
32//! ## Examples
33//!
34//! ```rust,no_run
35//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
36//! # pollster::block_on(async {
37//! use storekit::async_api::{AsyncProducts, AsyncAppTransaction};
38//!
39//! let products = AsyncProducts::fetch(["com.example.pro"])?.await?;
40//! println!("{} product(s) found", products.len());
41//!
42//! let app_tx = AsyncAppTransaction::shared().await?;
43//! println!("bundle: {}", app_tx.payload().bundle_id);
44//! # Ok::<(), Box<dyn std::error::Error>>(())
45//! # })?;
46//! # Ok(())
47//! # }
48//! ```
49
50use std::ffi::{c_void, CStr};
51use std::future::Future;
52use std::pin::Pin;
53use std::task::{Context, Poll};
54
55use doom_fish_utils::completion::{error_from_cstr, AsyncCompletion, AsyncCompletionFuture};
56use doom_fish_utils::panic_safe::catch_user_panic;
57
58use crate::app_transaction::{AppTransaction, AppTransactionPayload};
59use crate::error::StoreKitError;
60use crate::private::{cstring_from_str, json_cstring, take_string};
61use crate::product::{Product, ProductPayload};
62use crate::purchase_option::{PurchaseOption, PurchaseResult, PurchaseResultPayload};
63use crate::storefront::{Storefront, StorefrontPayload};
64use crate::verification_result::{VerificationResult, VerificationResultPayload};
65
66// ============================================================================
67// Internal helpers
68// ============================================================================
69
70/// Read a transient JSON C-string from a `*const c_void` result pointer.
71///
72/// The caller's Swift thunk passes a `&str`-borrowed `CStr` as `UnsafeRawPointer`.
73/// We copy it to an owned `String` immediately so the borrow lifetime in Swift
74/// is satisfied before the callback returns.
75///
76/// # Safety
77///
78/// `result` must be a valid, non-null pointer to a NUL-terminated C string that
79/// remains alive for the entire duration of this call.  The pointer is borrowed
80/// (not freed) — ownership stays with the Swift caller.
81unsafe fn json_from_result_ptr(result: *const c_void) -> String {
82    // SAFETY: caller guarantees result is a valid, NUL-terminated C string
83    // for the duration of this call.
84    CStr::from_ptr(result.cast::<i8>())
85        .to_string_lossy()
86        .into_owned()
87}
88
89// ============================================================================
90// AsyncProducts — Product.products(for:) async throws -> [Product]
91// ============================================================================
92
93extern "C" fn products_cb(result: *const c_void, error: *const i8, ctx: *mut c_void) {
94    catch_user_panic("products_cb", || {
95        if !error.is_null() {
96            // SAFETY: error is a NUL-terminated C string, valid for this callback invocation.
97            let msg = unsafe { error_from_cstr(error) };
98            // SAFETY: ctx is the Arc pointer from AsyncCompletion::create(); fired at most once.
99            unsafe { AsyncCompletion::<String>::complete_err(ctx, msg) };
100        } else if !result.is_null() {
101            // SAFETY: result is a NUL-terminated C string, valid for this callback invocation.
102            let json = unsafe { json_from_result_ptr(result) };
103            // SAFETY: ctx is the Arc pointer from AsyncCompletion::create(); fired at most once.
104            unsafe { AsyncCompletion::complete_ok(ctx, json) };
105        } else {
106            // SAFETY: ctx is the Arc pointer from AsyncCompletion::create(); fired at most once.
107            unsafe {
108                AsyncCompletion::<String>::complete_err(
109                    ctx,
110                    "no result from sk_products_async".into(),
111                );
112            };
113        }
114    });
115}
116
117/// Future for [`AsyncProducts::fetch`].
118pub struct ProductsFuture {
119    inner: AsyncCompletionFuture<String>,
120}
121
122impl std::fmt::Debug for ProductsFuture {
123    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
124        f.debug_struct("ProductsFuture").finish_non_exhaustive()
125    }
126}
127
128impl Future for ProductsFuture {
129    type Output = Result<Vec<Product>, StoreKitError>;
130
131    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
132        Pin::new(&mut self.inner).poll(cx).map(|r| {
133            let json = r.map_err(StoreKitError::Unknown)?;
134            let payloads: Vec<ProductPayload> = serde_json::from_str(&json).map_err(|e| {
135                StoreKitError::InvalidArgument(format!("failed to parse products JSON: {e}"))
136            })?;
137            payloads
138                .into_iter()
139                .map(ProductPayload::into_product)
140                .collect()
141        })
142    }
143}
144
145/// Async wrapper for `Product.products(for:)`.
146///
147/// Fetches products from the App Store for a set of product identifiers.
148///
149/// # Notes
150///
151/// On macOS Sandbox / Xcode previews you must configure a `StoreKit
152/// Configuration File` in your scheme for products to be returned.
153#[derive(Debug, Clone, Copy)]
154pub struct AsyncProducts;
155
156impl AsyncProducts {
157    /// Asynchronously fetch products for the given identifiers.
158    ///
159    /// Equivalent to `Product.products(for: identifiers)` in Swift.
160    ///
161    /// # Errors
162    ///
163    /// Returns an error if the App Store request fails or the product
164    /// identifiers cannot be encoded.
165    pub fn fetch<I, S>(identifiers: I) -> Result<ProductsFuture, StoreKitError>
166    where
167        I: IntoIterator<Item = S>,
168        S: AsRef<str>,
169    {
170        let ids: Vec<String> = identifiers
171            .into_iter()
172            .map(|s| s.as_ref().to_owned())
173            .collect();
174        let ids_json = json_cstring(&ids, "product identifiers")?;
175        let (future, ctx) = AsyncCompletion::create();
176        unsafe { crate::ffi::sk_products_async(ids_json.as_ptr(), products_cb, ctx) }
177        Ok(ProductsFuture { inner: future })
178    }
179}
180
181// ============================================================================
182// AsyncPurchase — Product.purchase(options:) async throws -> Product.PurchaseResult
183// ============================================================================
184
185/// Wrapper that carries the opaque Swift `SKPurchaseAsyncResult` pointer.
186/// `Send` is safe because the pointer is a retained Swift object with no
187/// thread-affinity restrictions after it has been constructed.
188struct RawPurchaseBox(*mut c_void);
189unsafe impl Send for RawPurchaseBox {}
190
191impl Drop for RawPurchaseBox {
192    fn drop(&mut self) {
193        if !self.0.is_null() {
194            // SAFETY: self.0 is a retained SKPurchaseAsyncResult that this wrapper
195            // uniquely owns.  Drop is the sole release point and runs exactly once.
196            unsafe { crate::ffi::sk_purchase_async_result_release(self.0) };
197        }
198    }
199}
200
201extern "C" fn purchase_cb(result: *const c_void, error: *const i8, ctx: *mut c_void) {
202    catch_user_panic("purchase_cb", || {
203        if !error.is_null() {
204            // SAFETY: error is a NUL-terminated C string, valid for this callback invocation.
205            let msg = unsafe { error_from_cstr(error) };
206            // SAFETY: ctx is the Arc pointer from AsyncCompletion::create(); fired at most once.
207            unsafe { AsyncCompletion::<RawPurchaseBox>::complete_err(ctx, msg) };
208        } else if !result.is_null() {
209            // result_ptr is a *retained* SKPurchaseAsyncResult — take ownership.
210            let boxed = RawPurchaseBox(result.cast_mut());
211            // SAFETY: ctx is the Arc pointer from AsyncCompletion::create(); fired at most once.
212            unsafe { AsyncCompletion::complete_ok(ctx, boxed) };
213        } else {
214            // SAFETY: ctx is the Arc pointer from AsyncCompletion::create(); fired at most once.
215            unsafe {
216                AsyncCompletion::<RawPurchaseBox>::complete_err(
217                    ctx,
218                    "no result from sk_product_purchase_async".into(),
219                );
220            };
221        }
222    });
223}
224
225/// Future for [`AsyncPurchase::buy`].
226pub struct PurchaseFuture {
227    inner: AsyncCompletionFuture<RawPurchaseBox>,
228}
229
230impl std::fmt::Debug for PurchaseFuture {
231    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
232        f.debug_struct("PurchaseFuture").finish_non_exhaustive()
233    }
234}
235
236impl Future for PurchaseFuture {
237    type Output = Result<PurchaseResult, StoreKitError>;
238
239    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
240        Pin::new(&mut self.inner).poll(cx).map(|r| {
241            let raw_box = r.map_err(StoreKitError::Unknown)?;
242            let ptr = raw_box.0;
243            // SAFETY: ptr is a retained Swift SKPurchaseAsyncResult owned by raw_box.
244            // raw_box's Drop impl calls sk_purchase_async_result_release, so the
245            // pointer is released even if extract_purchase_result panics or returns early.
246            let result = unsafe { extract_purchase_result(ptr) };
247            // raw_box drops here, releasing the pointer.
248            result
249        })
250    }
251}
252
253/// Extract `PurchaseResult` from a retained `SKPurchaseAsyncResult` pointer.
254///
255/// # Safety
256///
257/// `ptr` must be a valid, retained `SKPurchaseAsyncResult` pointer.
258unsafe fn extract_purchase_result(ptr: *mut c_void) -> Result<PurchaseResult, StoreKitError> {
259    let json_ptr = crate::ffi::sk_purchase_async_result_json(ptr);
260    let json = take_string(json_ptr).ok_or_else(|| {
261        StoreKitError::InvalidArgument("missing JSON from purchase async result".into())
262    })?;
263    let transaction_handle = crate::ffi::sk_purchase_async_result_take_handle(ptr);
264
265    let payload: PurchaseResultPayload = serde_json::from_str(&json).map_err(|e| {
266        if !transaction_handle.is_null() {
267            crate::ffi::sk_transaction_release(transaction_handle);
268        }
269        StoreKitError::InvalidArgument(format!(
270            "failed to parse purchase result JSON: {e}; payload={json}"
271        ))
272    })?;
273    payload.into_purchase_result(transaction_handle)
274}
275
276/// Async wrapper for `Product.purchase(options:)`.
277///
278/// Initiates an in-app purchase and resolves to the `PurchaseResult`.
279/// The purchase sheet is shown on the **main actor** (equivalent to Swift's
280/// `@MainActor`).
281///
282/// # Notes
283///
284/// - The future must be awaited until completion; cancellation is not
285///   supported by the `StoreKit` 2 purchase API.
286/// - `Transaction.currentEntitlements` and `Transaction.updates`
287///   (multi-fire streams) are deferred to Tier 2.
288#[derive(Debug, Clone, Copy)]
289pub struct AsyncPurchase;
290
291impl AsyncPurchase {
292    /// Asynchronously purchase a product.
293    ///
294    /// Equivalent to `product.purchase(options:)` in Swift.
295    ///
296    /// # Errors
297    ///
298    /// Returns an error if the product cannot be found, the purchase fails,
299    /// or the options cannot be encoded.
300    pub fn buy(product_id: &str, options: &[PurchaseOption]) -> Result<PurchaseFuture, StoreKitError> {
301        let id = cstring_from_str(product_id, "product id")?;
302        let opts = json_cstring(options, "purchase options")?;
303        let (future, ctx) = AsyncCompletion::create();
304        unsafe { crate::ffi::sk_product_purchase_async(id.as_ptr(), opts.as_ptr(), purchase_cb, ctx) }
305        Ok(PurchaseFuture { inner: future })
306    }
307}
308
309// ============================================================================
310// AsyncAppStore — AppStore.requestReview() / showManageSubscriptions()
311// ============================================================================
312
313extern "C" fn void_cb(_result: *const c_void, error: *const i8, ctx: *mut c_void) {
314    catch_user_panic("void_cb", || {
315        if error.is_null() {
316            // Ignore result_ptr; void APIs use a sentinel 0x1 which we don't need.
317            // SAFETY: ctx is the Arc pointer from AsyncCompletion::create(); fired at most once.
318            unsafe { AsyncCompletion::complete_ok(ctx, ()) };
319        } else {
320            // SAFETY: error is a NUL-terminated C string, valid for this callback invocation.
321            let msg = unsafe { error_from_cstr(error) };
322            // SAFETY: ctx is the Arc pointer from AsyncCompletion::create(); fired at most once.
323            unsafe { AsyncCompletion::<()>::complete_err(ctx, msg) };
324        }
325    });
326}
327
328/// Future for [`AsyncAppStore::request_review`].
329pub struct RequestReviewFuture {
330    inner: AsyncCompletionFuture<()>,
331}
332
333impl std::fmt::Debug for RequestReviewFuture {
334    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
335        f.debug_struct("RequestReviewFuture").finish_non_exhaustive()
336    }
337}
338
339impl Future for RequestReviewFuture {
340    type Output = Result<(), StoreKitError>;
341
342    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
343        Pin::new(&mut self.inner)
344            .poll(cx)
345            .map(|r| r.map_err(StoreKitError::Unknown))
346    }
347}
348
349/// Future for [`AsyncAppStore::show_manage_subscriptions`].
350pub struct ShowManageSubscriptionsFuture {
351    inner: AsyncCompletionFuture<()>,
352}
353
354impl std::fmt::Debug for ShowManageSubscriptionsFuture {
355    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
356        f.debug_struct("ShowManageSubscriptionsFuture")
357            .finish_non_exhaustive()
358    }
359}
360
361impl Future for ShowManageSubscriptionsFuture {
362    type Output = Result<(), StoreKitError>;
363
364    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
365        Pin::new(&mut self.inner)
366            .poll(cx)
367            .map(|r| r.map_err(StoreKitError::NotSupported))
368    }
369}
370
371/// Async wrapper for `AppStore` UI APIs.
372///
373/// # Notes
374///
375/// - [`AsyncAppStore::request_review`] requires macOS 13.0+ and an
376///   `NSViewController`-backed window.
377/// - [`AsyncAppStore::show_manage_subscriptions`] is scene-based (`SwiftUI`)
378///   and always returns a `NotSupported` error on macOS.  Consider opening
379///   the `itms-apps://` URL directly for `AppKit` apps.
380#[derive(Debug, Clone, Copy, Default)]
381pub struct AsyncAppStore;
382
383impl AsyncAppStore {
384    /// Asynchronously prompt the user for an App Store review.
385    ///
386    /// Equivalent to `AppStore.requestReview(in:)` in Swift.
387    ///
388    /// # Errors
389    ///
390    /// Returns a `NotSupported` error when:
391    /// - Running on macOS < 13.0.
392    /// - No `NSViewController`-backed key window is available.
393    #[must_use = "futures do nothing unless polled"]
394    pub fn request_review() -> RequestReviewFuture {
395        let (future, ctx) = AsyncCompletion::create();
396        unsafe { crate::ffi::sk_app_store_request_review_async(void_cb, ctx) }
397        RequestReviewFuture { inner: future }
398    }
399
400    /// Asynchronously show the "Manage Subscriptions" sheet.
401    ///
402    /// Equivalent to `AppStore.showManageSubscriptions(in:)` in Swift.
403    ///
404    /// # Errors
405    ///
406    /// Always returns a `NotSupported` error on macOS because
407    /// `showManageSubscriptions(in:)` is scene-based and unavailable in
408    /// the macOS `StoreKit` SDK.
409    #[must_use = "futures do nothing unless polled"]
410    pub fn show_manage_subscriptions() -> ShowManageSubscriptionsFuture {
411        let (future, ctx) = AsyncCompletion::create();
412        unsafe { crate::ffi::sk_app_store_show_manage_subscriptions_async(void_cb, ctx) }
413        ShowManageSubscriptionsFuture { inner: future }
414    }
415}
416
417// ============================================================================
418// AsyncAppTransaction — AppTransaction.shared async throws
419// ============================================================================
420
421extern "C" fn app_transaction_cb(result: *const c_void, error: *const i8, ctx: *mut c_void) {
422    catch_user_panic("app_transaction_cb", || {
423        if !error.is_null() {
424            // SAFETY: error is a NUL-terminated C string, valid for this callback invocation.
425            let msg = unsafe { error_from_cstr(error) };
426            // SAFETY: ctx is the Arc pointer from AsyncCompletion::create(); fired at most once.
427            unsafe { AsyncCompletion::<String>::complete_err(ctx, msg) };
428        } else if !result.is_null() {
429            // SAFETY: result is a NUL-terminated C string, valid for this callback invocation.
430            let json = unsafe { json_from_result_ptr(result) };
431            // SAFETY: ctx is the Arc pointer from AsyncCompletion::create(); fired at most once.
432            unsafe { AsyncCompletion::complete_ok(ctx, json) };
433        } else {
434            // SAFETY: ctx is the Arc pointer from AsyncCompletion::create(); fired at most once.
435            unsafe {
436                AsyncCompletion::<String>::complete_err(
437                    ctx,
438                    "no result from sk_app_transaction_shared_async".into(),
439                );
440            };
441        }
442    });
443}
444
445/// Future for [`AsyncAppTransaction::shared`].
446pub struct AppTransactionFuture {
447    inner: AsyncCompletionFuture<String>,
448}
449
450impl std::fmt::Debug for AppTransactionFuture {
451    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
452        f.debug_struct("AppTransactionFuture").finish_non_exhaustive()
453    }
454}
455
456impl Future for AppTransactionFuture {
457    type Output = Result<VerificationResult<AppTransaction>, StoreKitError>;
458
459    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
460        Pin::new(&mut self.inner).poll(cx).map(|r| {
461            let json = r.map_err(StoreKitError::Unknown)?;
462            let payload: VerificationResultPayload<AppTransactionPayload> =
463                serde_json::from_str(&json).map_err(|e| {
464                    StoreKitError::InvalidArgument(format!(
465                        "failed to parse app transaction JSON: {e}"
466                    ))
467                })?;
468            payload.into_result(AppTransactionPayload::into_app_transaction)
469        })
470    }
471}
472
473/// Async wrapper for `AppTransaction.shared`.
474///
475/// Returns a `VerificationResult<AppTransaction>` that can be verified
476/// with `.verified()` to confirm the transaction's authenticity.
477///
478/// Requires macOS 13.0+.
479///
480/// # Notes
481///
482/// `AppTransaction.shared` may prompt the user to authenticate with the
483/// App Store, so it should be awaited before presenting any gated content.
484#[derive(Debug, Clone, Copy)]
485pub struct AsyncAppTransaction;
486
487impl AsyncAppTransaction {
488    /// Asynchronously fetch `AppTransaction.shared`.
489    ///
490    /// Equivalent to `AppTransaction.shared` in Swift.
491    ///
492    /// # Errors
493    ///
494    /// Returns a `NotSupported` error on macOS < 13.0.
495    #[must_use = "futures do nothing unless polled"]
496    pub fn shared() -> AppTransactionFuture {
497        let (future, ctx) = AsyncCompletion::create();
498        unsafe { crate::ffi::sk_app_transaction_shared_async(app_transaction_cb, ctx) }
499        AppTransactionFuture { inner: future }
500    }
501}
502
503// ============================================================================
504// AsyncStorefront — Storefront.current async
505// ============================================================================
506
507/// Wrapper that carries the optional storefront JSON.
508struct StorefrontResult(Option<String>);
509
510extern "C" fn storefront_cb(result: *const c_void, error: *const i8, ctx: *mut c_void) {
511    catch_user_panic("storefront_cb", || {
512        if !error.is_null() {
513            // SAFETY: error is a NUL-terminated C string, valid for this callback invocation.
514            let msg = unsafe { error_from_cstr(error) };
515            // SAFETY: ctx is the Arc pointer from AsyncCompletion::create(); fired at most once.
516            unsafe { AsyncCompletion::<StorefrontResult>::complete_err(ctx, msg) };
517        } else if !result.is_null() {
518            // SAFETY: result is a NUL-terminated C string, valid for this callback invocation.
519            let json = unsafe { json_from_result_ptr(result) };
520            // SAFETY: ctx is the Arc pointer from AsyncCompletion::create(); fired at most once.
521            unsafe { AsyncCompletion::complete_ok(ctx, StorefrontResult(Some(json))) };
522        } else {
523            // nil result_ptr means success but nil storefront
524            // SAFETY: ctx is the Arc pointer from AsyncCompletion::create(); fired at most once.
525            unsafe { AsyncCompletion::complete_ok(ctx, StorefrontResult(None)) };
526        }
527    });
528}
529
530/// JSON envelope emitted by `sk_storefront_current_async`.
531#[derive(serde::Deserialize)]
532struct StorefrontCurrentPayload {
533    storefront: Option<StorefrontPayload>,
534}
535
536/// Future for [`AsyncStorefront::current`].
537pub struct StorefrontCurrentFuture {
538    inner: AsyncCompletionFuture<StorefrontResult>,
539}
540
541impl std::fmt::Debug for StorefrontCurrentFuture {
542    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
543        f.debug_struct("StorefrontCurrentFuture")
544            .finish_non_exhaustive()
545    }
546}
547
548impl Future for StorefrontCurrentFuture {
549    type Output = Result<Option<Storefront>, StoreKitError>;
550
551    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
552        Pin::new(&mut self.inner).poll(cx).map(|r| {
553            let StorefrontResult(maybe_json) = r.map_err(StoreKitError::Unknown)?;
554            let Some(json) = maybe_json else { return Ok(None) };
555            let wrapper: StorefrontCurrentPayload =
556                serde_json::from_str(&json).map_err(|e| {
557                    StoreKitError::InvalidArgument(format!(
558                        "failed to parse storefront JSON: {e}"
559                    ))
560                })?;
561            Ok(wrapper.storefront.map(StorefrontPayload::into_storefront))
562        })
563    }
564}
565
566/// Async wrapper for `Storefront.current`.
567///
568/// Returns the current App Store storefront, or `None` if no storefront is
569/// available (e.g. the device is not connected to the App Store).
570#[derive(Debug, Clone, Copy)]
571pub struct AsyncStorefront;
572
573impl AsyncStorefront {
574    /// Asynchronously fetch `Storefront.current`.
575    ///
576    /// Equivalent to `Storefront.current` in Swift.
577    #[must_use = "futures do nothing unless polled"]
578    pub fn current() -> StorefrontCurrentFuture {
579        let (future, ctx) = AsyncCompletion::create();
580        unsafe { crate::ffi::sk_storefront_current_async(storefront_cb, ctx) }
581        StorefrontCurrentFuture { inner: future }
582    }
583}