storekit-rs 0.3.1

Safe Rust bindings for Apple's StoreKit framework — in-app purchases and transaction streams on macOS
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
//! Async API for `StoreKit` — Tier 1 Future wrappers.
//!
//! This module requires the **`async`** Cargo feature:
//! ```toml
//! storekit-rs = { version = "0.3", features = ["async"] }
//! ```
//!
//! Every public type is an executor-agnostic [`Future`] backed by a
//! `doom_fish_utils` completion handler.  The futures work with any async
//! runtime (`tokio`, `async-std`, `smol`, `pollster`, …).
//!
//! ## Available types
//!
//! | Type | Description |
//! |------|-------------|
//! | [`AsyncProducts`] | Fetch products by identifier |
//! | [`AsyncPurchase`] | Purchase a product |
//! | [`AsyncAppStore`] | Request review / manage subscriptions |
//! | [`AsyncAppTransaction`] | Fetch the app transaction |
//! | [`AsyncStorefront`] | Fetch the current storefront |
//!
//! ## `AsyncSequence` APIs — deferred to Tier 2
//!
//! The following `StoreKit` APIs expose `AsyncSequence` (multi-fire streams).
//! They are intentionally **not** included here; they will be wrapped as
//! `Stream` types in the Tier 2 rollout:
//!
//! - `Transaction.updates` — use [`crate::transaction::TransactionStream`] in the meantime
//! - `Transaction.currentEntitlements` — use [`crate::transaction::TransactionStream`]
//! - `Transaction.unfinished` — use [`crate::transaction::TransactionStream`]
//!
//! ## Examples
//!
//! ```rust,no_run
//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
//! # pollster::block_on(async {
//! use storekit::async_api::{AsyncProducts, AsyncAppTransaction};
//!
//! let products = AsyncProducts::fetch(["com.example.pro"])?.await?;
//! println!("{} product(s) found", products.len());
//!
//! let app_tx = AsyncAppTransaction::shared().await?;
//! println!("bundle: {}", app_tx.payload().bundle_id);
//! # Ok::<(), Box<dyn std::error::Error>>(())
//! # })?;
//! # Ok(())
//! # }
//! ```

use std::ffi::{c_void, CStr};
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};

use doom_fish_utils::completion::{error_from_cstr, AsyncCompletion, AsyncCompletionFuture};
use doom_fish_utils::panic_safe::catch_user_panic;

use crate::app_transaction::{AppTransaction, AppTransactionPayload};
use crate::error::StoreKitError;
use crate::private::{cstring_from_str, json_cstring, take_string};
use crate::product::{Product, ProductPayload};
use crate::purchase_option::{PurchaseOption, PurchaseResult, PurchaseResultPayload};
use crate::storefront::{Storefront, StorefrontPayload};
use crate::verification_result::{VerificationResult, VerificationResultPayload};

// ============================================================================
// Internal helpers
// ============================================================================

/// Read a transient JSON C-string from a `*const c_void` result pointer.
///
/// The caller's Swift thunk passes a `&str`-borrowed `CStr` as `UnsafeRawPointer`.
/// We copy it to an owned `String` immediately so the borrow lifetime in Swift
/// is satisfied before the callback returns.
///
/// # Safety
///
/// `result` must be a valid, non-null pointer to a NUL-terminated C string that
/// remains alive for the entire duration of this call.  The pointer is borrowed
/// (not freed) — ownership stays with the Swift caller.
unsafe fn json_from_result_ptr(result: *const c_void) -> String {
    // SAFETY: caller guarantees result is a valid, NUL-terminated C string
    // for the duration of this call.
    CStr::from_ptr(result.cast::<i8>())
        .to_string_lossy()
        .into_owned()
}

// ============================================================================
// AsyncProducts — Product.products(for:) async throws -> [Product]
// ============================================================================

extern "C" fn products_cb(result: *const c_void, error: *const i8, ctx: *mut c_void) {
    catch_user_panic("products_cb", || {
        if !error.is_null() {
            // SAFETY: error is a NUL-terminated C string, valid for this callback invocation.
            let msg = unsafe { error_from_cstr(error) };
            // SAFETY: ctx is the Arc pointer from AsyncCompletion::create(); fired at most once.
            unsafe { AsyncCompletion::<String>::complete_err(ctx, msg) };
        } else if !result.is_null() {
            // SAFETY: result is a NUL-terminated C string, valid for this callback invocation.
            let json = unsafe { json_from_result_ptr(result) };
            // SAFETY: ctx is the Arc pointer from AsyncCompletion::create(); fired at most once.
            unsafe { AsyncCompletion::complete_ok(ctx, json) };
        } else {
            // SAFETY: ctx is the Arc pointer from AsyncCompletion::create(); fired at most once.
            unsafe {
                AsyncCompletion::<String>::complete_err(
                    ctx,
                    "no result from sk_products_async".into(),
                );
            };
        }
    });
}

/// Future for [`AsyncProducts::fetch`].
pub struct ProductsFuture {
    inner: AsyncCompletionFuture<String>,
}

impl std::fmt::Debug for ProductsFuture {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("ProductsFuture").finish_non_exhaustive()
    }
}

impl Future for ProductsFuture {
    type Output = Result<Vec<Product>, StoreKitError>;

    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
        Pin::new(&mut self.inner).poll(cx).map(|r| {
            let json = r.map_err(StoreKitError::Unknown)?;
            let payloads: Vec<ProductPayload> = serde_json::from_str(&json).map_err(|e| {
                StoreKitError::InvalidArgument(format!("failed to parse products JSON: {e}"))
            })?;
            payloads
                .into_iter()
                .map(ProductPayload::into_product)
                .collect()
        })
    }
}

/// Async wrapper for `Product.products(for:)`.
///
/// Fetches products from the App Store for a set of product identifiers.
///
/// # Notes
///
/// On macOS Sandbox / Xcode previews you must configure a `StoreKit
/// Configuration File` in your scheme for products to be returned.
#[derive(Debug, Clone, Copy)]
pub struct AsyncProducts;

impl AsyncProducts {
    /// Asynchronously fetch products for the given identifiers.
    ///
    /// Equivalent to `Product.products(for: identifiers)` in Swift.
    ///
    /// # Errors
    ///
    /// Returns an error if the App Store request fails or the product
    /// identifiers cannot be encoded.
    pub fn fetch<I, S>(identifiers: I) -> Result<ProductsFuture, StoreKitError>
    where
        I: IntoIterator<Item = S>,
        S: AsRef<str>,
    {
        let ids: Vec<String> = identifiers
            .into_iter()
            .map(|s| s.as_ref().to_owned())
            .collect();
        let ids_json = json_cstring(&ids, "product identifiers")?;
        let (future, ctx) = AsyncCompletion::create();
        unsafe { crate::ffi::sk_products_async(ids_json.as_ptr(), products_cb, ctx) }
        Ok(ProductsFuture { inner: future })
    }
}

// ============================================================================
// AsyncPurchase — Product.purchase(options:) async throws -> Product.PurchaseResult
// ============================================================================

/// Wrapper that carries the opaque Swift `SKPurchaseAsyncResult` pointer.
/// `Send` is safe because the pointer is a retained Swift object with no
/// thread-affinity restrictions after it has been constructed.
struct RawPurchaseBox(*mut c_void);
unsafe impl Send for RawPurchaseBox {}

impl Drop for RawPurchaseBox {
    fn drop(&mut self) {
        if !self.0.is_null() {
            // SAFETY: self.0 is a retained SKPurchaseAsyncResult that this wrapper
            // uniquely owns.  Drop is the sole release point and runs exactly once.
            unsafe { crate::ffi::sk_purchase_async_result_release(self.0) };
        }
    }
}

extern "C" fn purchase_cb(result: *const c_void, error: *const i8, ctx: *mut c_void) {
    catch_user_panic("purchase_cb", || {
        if !error.is_null() {
            // SAFETY: error is a NUL-terminated C string, valid for this callback invocation.
            let msg = unsafe { error_from_cstr(error) };
            // SAFETY: ctx is the Arc pointer from AsyncCompletion::create(); fired at most once.
            unsafe { AsyncCompletion::<RawPurchaseBox>::complete_err(ctx, msg) };
        } else if !result.is_null() {
            // result_ptr is a *retained* SKPurchaseAsyncResult — take ownership.
            let boxed = RawPurchaseBox(result.cast_mut());
            // SAFETY: ctx is the Arc pointer from AsyncCompletion::create(); fired at most once.
            unsafe { AsyncCompletion::complete_ok(ctx, boxed) };
        } else {
            // SAFETY: ctx is the Arc pointer from AsyncCompletion::create(); fired at most once.
            unsafe {
                AsyncCompletion::<RawPurchaseBox>::complete_err(
                    ctx,
                    "no result from sk_product_purchase_async".into(),
                );
            };
        }
    });
}

/// Future for [`AsyncPurchase::buy`].
pub struct PurchaseFuture {
    inner: AsyncCompletionFuture<RawPurchaseBox>,
}

impl std::fmt::Debug for PurchaseFuture {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("PurchaseFuture").finish_non_exhaustive()
    }
}

impl Future for PurchaseFuture {
    type Output = Result<PurchaseResult, StoreKitError>;

    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
        Pin::new(&mut self.inner).poll(cx).map(|r| {
            let raw_box = r.map_err(StoreKitError::Unknown)?;
            let ptr = raw_box.0;
            // SAFETY: ptr is a retained Swift SKPurchaseAsyncResult owned by raw_box.
            // raw_box's Drop impl calls sk_purchase_async_result_release, so the
            // pointer is released even if extract_purchase_result panics or returns early.
            let result = unsafe { extract_purchase_result(ptr) };
            // raw_box drops here, releasing the pointer.
            result
        })
    }
}

/// Extract `PurchaseResult` from a retained `SKPurchaseAsyncResult` pointer.
///
/// # Safety
///
/// `ptr` must be a valid, retained `SKPurchaseAsyncResult` pointer.
unsafe fn extract_purchase_result(ptr: *mut c_void) -> Result<PurchaseResult, StoreKitError> {
    let json_ptr = crate::ffi::sk_purchase_async_result_json(ptr);
    let json = take_string(json_ptr).ok_or_else(|| {
        StoreKitError::InvalidArgument("missing JSON from purchase async result".into())
    })?;
    let transaction_handle = crate::ffi::sk_purchase_async_result_take_handle(ptr);

    let payload: PurchaseResultPayload = serde_json::from_str(&json).map_err(|e| {
        if !transaction_handle.is_null() {
            crate::ffi::sk_transaction_release(transaction_handle);
        }
        StoreKitError::InvalidArgument(format!(
            "failed to parse purchase result JSON: {e}; payload={json}"
        ))
    })?;
    payload.into_purchase_result(transaction_handle)
}

/// Async wrapper for `Product.purchase(options:)`.
///
/// Initiates an in-app purchase and resolves to the `PurchaseResult`.
/// The purchase sheet is shown on the **main actor** (equivalent to Swift's
/// `@MainActor`).
///
/// # Notes
///
/// - The future must be awaited until completion; cancellation is not
///   supported by the `StoreKit` 2 purchase API.
/// - `Transaction.currentEntitlements` and `Transaction.updates`
///   (multi-fire streams) are deferred to Tier 2.
#[derive(Debug, Clone, Copy)]
pub struct AsyncPurchase;

impl AsyncPurchase {
    /// Asynchronously purchase a product.
    ///
    /// Equivalent to `product.purchase(options:)` in Swift.
    ///
    /// # Errors
    ///
    /// Returns an error if the product cannot be found, the purchase fails,
    /// or the options cannot be encoded.
    pub fn buy(product_id: &str, options: &[PurchaseOption]) -> Result<PurchaseFuture, StoreKitError> {
        let id = cstring_from_str(product_id, "product id")?;
        let opts = json_cstring(options, "purchase options")?;
        let (future, ctx) = AsyncCompletion::create();
        unsafe { crate::ffi::sk_product_purchase_async(id.as_ptr(), opts.as_ptr(), purchase_cb, ctx) }
        Ok(PurchaseFuture { inner: future })
    }
}

// ============================================================================
// AsyncAppStore — AppStore.requestReview() / showManageSubscriptions()
// ============================================================================

extern "C" fn void_cb(_result: *const c_void, error: *const i8, ctx: *mut c_void) {
    catch_user_panic("void_cb", || {
        if error.is_null() {
            // Ignore result_ptr; void APIs use a sentinel 0x1 which we don't need.
            // SAFETY: ctx is the Arc pointer from AsyncCompletion::create(); fired at most once.
            unsafe { AsyncCompletion::complete_ok(ctx, ()) };
        } else {
            // SAFETY: error is a NUL-terminated C string, valid for this callback invocation.
            let msg = unsafe { error_from_cstr(error) };
            // SAFETY: ctx is the Arc pointer from AsyncCompletion::create(); fired at most once.
            unsafe { AsyncCompletion::<()>::complete_err(ctx, msg) };
        }
    });
}

/// Future for [`AsyncAppStore::request_review`].
pub struct RequestReviewFuture {
    inner: AsyncCompletionFuture<()>,
}

impl std::fmt::Debug for RequestReviewFuture {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("RequestReviewFuture").finish_non_exhaustive()
    }
}

impl Future for RequestReviewFuture {
    type Output = Result<(), StoreKitError>;

    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
        Pin::new(&mut self.inner)
            .poll(cx)
            .map(|r| r.map_err(StoreKitError::Unknown))
    }
}

/// Future for [`AsyncAppStore::show_manage_subscriptions`].
pub struct ShowManageSubscriptionsFuture {
    inner: AsyncCompletionFuture<()>,
}

impl std::fmt::Debug for ShowManageSubscriptionsFuture {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("ShowManageSubscriptionsFuture")
            .finish_non_exhaustive()
    }
}

impl Future for ShowManageSubscriptionsFuture {
    type Output = Result<(), StoreKitError>;

    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
        Pin::new(&mut self.inner)
            .poll(cx)
            .map(|r| r.map_err(StoreKitError::NotSupported))
    }
}

/// Async wrapper for `AppStore` UI APIs.
///
/// # Notes
///
/// - [`AsyncAppStore::request_review`] requires macOS 13.0+ and an
///   `NSViewController`-backed window.
/// - [`AsyncAppStore::show_manage_subscriptions`] is scene-based (`SwiftUI`)
///   and always returns a `NotSupported` error on macOS.  Consider opening
///   the `itms-apps://` URL directly for `AppKit` apps.
#[derive(Debug, Clone, Copy, Default)]
pub struct AsyncAppStore;

impl AsyncAppStore {
    /// Asynchronously prompt the user for an App Store review.
    ///
    /// Equivalent to `AppStore.requestReview(in:)` in Swift.
    ///
    /// # Errors
    ///
    /// Returns a `NotSupported` error when:
    /// - Running on macOS < 13.0.
    /// - No `NSViewController`-backed key window is available.
    #[must_use = "futures do nothing unless polled"]
    pub fn request_review() -> RequestReviewFuture {
        let (future, ctx) = AsyncCompletion::create();
        unsafe { crate::ffi::sk_app_store_request_review_async(void_cb, ctx) }
        RequestReviewFuture { inner: future }
    }

    /// Asynchronously show the "Manage Subscriptions" sheet.
    ///
    /// Equivalent to `AppStore.showManageSubscriptions(in:)` in Swift.
    ///
    /// # Errors
    ///
    /// Always returns a `NotSupported` error on macOS because
    /// `showManageSubscriptions(in:)` is scene-based and unavailable in
    /// the macOS `StoreKit` SDK.
    #[must_use = "futures do nothing unless polled"]
    pub fn show_manage_subscriptions() -> ShowManageSubscriptionsFuture {
        let (future, ctx) = AsyncCompletion::create();
        unsafe { crate::ffi::sk_app_store_show_manage_subscriptions_async(void_cb, ctx) }
        ShowManageSubscriptionsFuture { inner: future }
    }
}

// ============================================================================
// AsyncAppTransaction — AppTransaction.shared async throws
// ============================================================================

extern "C" fn app_transaction_cb(result: *const c_void, error: *const i8, ctx: *mut c_void) {
    catch_user_panic("app_transaction_cb", || {
        if !error.is_null() {
            // SAFETY: error is a NUL-terminated C string, valid for this callback invocation.
            let msg = unsafe { error_from_cstr(error) };
            // SAFETY: ctx is the Arc pointer from AsyncCompletion::create(); fired at most once.
            unsafe { AsyncCompletion::<String>::complete_err(ctx, msg) };
        } else if !result.is_null() {
            // SAFETY: result is a NUL-terminated C string, valid for this callback invocation.
            let json = unsafe { json_from_result_ptr(result) };
            // SAFETY: ctx is the Arc pointer from AsyncCompletion::create(); fired at most once.
            unsafe { AsyncCompletion::complete_ok(ctx, json) };
        } else {
            // SAFETY: ctx is the Arc pointer from AsyncCompletion::create(); fired at most once.
            unsafe {
                AsyncCompletion::<String>::complete_err(
                    ctx,
                    "no result from sk_app_transaction_shared_async".into(),
                );
            };
        }
    });
}

/// Future for [`AsyncAppTransaction::shared`].
pub struct AppTransactionFuture {
    inner: AsyncCompletionFuture<String>,
}

impl std::fmt::Debug for AppTransactionFuture {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("AppTransactionFuture").finish_non_exhaustive()
    }
}

impl Future for AppTransactionFuture {
    type Output = Result<VerificationResult<AppTransaction>, StoreKitError>;

    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
        Pin::new(&mut self.inner).poll(cx).map(|r| {
            let json = r.map_err(StoreKitError::Unknown)?;
            let payload: VerificationResultPayload<AppTransactionPayload> =
                serde_json::from_str(&json).map_err(|e| {
                    StoreKitError::InvalidArgument(format!(
                        "failed to parse app transaction JSON: {e}"
                    ))
                })?;
            payload.into_result(AppTransactionPayload::into_app_transaction)
        })
    }
}

/// Async wrapper for `AppTransaction.shared`.
///
/// Returns a `VerificationResult<AppTransaction>` that can be verified
/// with `.verified()` to confirm the transaction's authenticity.
///
/// Requires macOS 13.0+.
///
/// # Notes
///
/// `AppTransaction.shared` may prompt the user to authenticate with the
/// App Store, so it should be awaited before presenting any gated content.
#[derive(Debug, Clone, Copy)]
pub struct AsyncAppTransaction;

impl AsyncAppTransaction {
    /// Asynchronously fetch `AppTransaction.shared`.
    ///
    /// Equivalent to `AppTransaction.shared` in Swift.
    ///
    /// # Errors
    ///
    /// Returns a `NotSupported` error on macOS < 13.0.
    #[must_use = "futures do nothing unless polled"]
    pub fn shared() -> AppTransactionFuture {
        let (future, ctx) = AsyncCompletion::create();
        unsafe { crate::ffi::sk_app_transaction_shared_async(app_transaction_cb, ctx) }
        AppTransactionFuture { inner: future }
    }
}

// ============================================================================
// AsyncStorefront — Storefront.current async
// ============================================================================

/// Wrapper that carries the optional storefront JSON.
struct StorefrontResult(Option<String>);

extern "C" fn storefront_cb(result: *const c_void, error: *const i8, ctx: *mut c_void) {
    catch_user_panic("storefront_cb", || {
        if !error.is_null() {
            // SAFETY: error is a NUL-terminated C string, valid for this callback invocation.
            let msg = unsafe { error_from_cstr(error) };
            // SAFETY: ctx is the Arc pointer from AsyncCompletion::create(); fired at most once.
            unsafe { AsyncCompletion::<StorefrontResult>::complete_err(ctx, msg) };
        } else if !result.is_null() {
            // SAFETY: result is a NUL-terminated C string, valid for this callback invocation.
            let json = unsafe { json_from_result_ptr(result) };
            // SAFETY: ctx is the Arc pointer from AsyncCompletion::create(); fired at most once.
            unsafe { AsyncCompletion::complete_ok(ctx, StorefrontResult(Some(json))) };
        } else {
            // nil result_ptr means success but nil storefront
            // SAFETY: ctx is the Arc pointer from AsyncCompletion::create(); fired at most once.
            unsafe { AsyncCompletion::complete_ok(ctx, StorefrontResult(None)) };
        }
    });
}

/// JSON envelope emitted by `sk_storefront_current_async`.
#[derive(serde::Deserialize)]
struct StorefrontCurrentPayload {
    storefront: Option<StorefrontPayload>,
}

/// Future for [`AsyncStorefront::current`].
pub struct StorefrontCurrentFuture {
    inner: AsyncCompletionFuture<StorefrontResult>,
}

impl std::fmt::Debug for StorefrontCurrentFuture {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("StorefrontCurrentFuture")
            .finish_non_exhaustive()
    }
}

impl Future for StorefrontCurrentFuture {
    type Output = Result<Option<Storefront>, StoreKitError>;

    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
        Pin::new(&mut self.inner).poll(cx).map(|r| {
            let StorefrontResult(maybe_json) = r.map_err(StoreKitError::Unknown)?;
            let Some(json) = maybe_json else { return Ok(None) };
            let wrapper: StorefrontCurrentPayload =
                serde_json::from_str(&json).map_err(|e| {
                    StoreKitError::InvalidArgument(format!(
                        "failed to parse storefront JSON: {e}"
                    ))
                })?;
            Ok(wrapper.storefront.map(StorefrontPayload::into_storefront))
        })
    }
}

/// Async wrapper for `Storefront.current`.
///
/// Returns the current App Store storefront, or `None` if no storefront is
/// available (e.g. the device is not connected to the App Store).
#[derive(Debug, Clone, Copy)]
pub struct AsyncStorefront;

impl AsyncStorefront {
    /// Asynchronously fetch `Storefront.current`.
    ///
    /// Equivalent to `Storefront.current` in Swift.
    #[must_use = "futures do nothing unless polled"]
    pub fn current() -> StorefrontCurrentFuture {
        let (future, ctx) = AsyncCompletion::create();
        unsafe { crate::ffi::sk_storefront_current_async(storefront_cb, ctx) }
        StorefrontCurrentFuture { inner: future }
    }
}