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}