Skip to main content

storekit/
purchase_intent.rs

1use core::ffi::c_void;
2use core::ptr;
3use std::ptr::NonNull;
4use std::time::Duration;
5
6use serde::Deserialize;
7
8use crate::error::StoreKitError;
9use crate::ffi;
10use crate::private::{duration_to_timeout_ms, error_from_status, parse_json_ptr};
11use crate::product::{Product, ProductPayload};
12use crate::subscription::{SubscriptionOffer, SubscriptionOfferPayload};
13
14#[derive(Debug, Clone, PartialEq, Eq)]
15/// Wraps `StoreKit.PurchaseIntent`.
16pub struct PurchaseIntent {
17    /// `StoreKit`-provided `product` value.
18    pub product: Product,
19    /// Offer metadata reported by `StoreKit`.
20    pub offer: Option<SubscriptionOffer>,
21}
22
23impl PurchaseIntent {
24    /// Returns the product identifier reported by `StoreKit` for this purchase intent.
25    pub fn id(&self) -> &str {
26        &self.product.id
27    }
28
29    /// Creates a stream backed by `StoreKit` purchase intents.
30    pub fn intents() -> Result<PurchaseIntentStream, StoreKitError> {
31        PurchaseIntentStream::new()
32    }
33}
34
35#[derive(Debug)]
36/// Wraps the `StoreKit` purchase intent stream.
37pub struct PurchaseIntentStream {
38    handle: NonNull<c_void>,
39    finished: bool,
40}
41
42impl Drop for PurchaseIntentStream {
43    fn drop(&mut self) {
44        unsafe { ffi::sk_purchase_intent_stream_release(self.handle.as_ptr()) };
45    }
46}
47
48impl PurchaseIntentStream {
49    fn new() -> Result<Self, StoreKitError> {
50        let mut error_message = ptr::null_mut();
51        let handle = unsafe { ffi::sk_purchase_intent_stream_create(&mut error_message) };
52        let handle = NonNull::new(handle)
53            .ok_or_else(|| unsafe { error_from_status(ffi::status::UNKNOWN, error_message) })?;
54        Ok(Self {
55            handle,
56            finished: false,
57        })
58    }
59
60    /// Returns whether this `StoreKit` stream has reached the end of the sequence.
61    pub const fn is_finished(&self) -> bool {
62        self.finished
63    }
64
65    #[allow(clippy::should_implement_trait)]
66    /// Waits for the next value from the `StoreKit` stream using the default timeout.
67    pub fn next(&mut self) -> Result<Option<PurchaseIntent>, StoreKitError> {
68        self.next_timeout(Duration::from_secs(30))
69    }
70
71    /// Waits for the next value from the `StoreKit` stream up to the supplied timeout.
72    pub fn next_timeout(
73        &mut self,
74        timeout: Duration,
75    ) -> Result<Option<PurchaseIntent>, StoreKitError> {
76        let mut payload_json = ptr::null_mut();
77        let mut error_message = ptr::null_mut();
78        let status = unsafe {
79            ffi::sk_purchase_intent_stream_next(
80                self.handle.as_ptr(),
81                duration_to_timeout_ms(timeout),
82                &mut payload_json,
83                &mut error_message,
84            )
85        };
86
87        match status {
88            ffi::status::OK => {
89                let payload = unsafe {
90                    parse_json_ptr::<PurchaseIntentPayload>(payload_json, "purchase intent")
91                }?;
92                payload.into_purchase_intent().map(Some)
93            }
94            ffi::status::END_OF_STREAM => {
95                self.finished = true;
96                Ok(None)
97            }
98            ffi::status::TIMED_OUT => Ok(None),
99            _ => Err(unsafe { error_from_status(status, error_message) }),
100        }
101    }
102}
103
104#[derive(Debug, Deserialize)]
105struct PurchaseIntentPayload {
106    product: ProductPayload,
107    offer: Option<SubscriptionOfferPayload>,
108}
109
110impl PurchaseIntentPayload {
111    fn into_purchase_intent(self) -> Result<PurchaseIntent, StoreKitError> {
112        Ok(PurchaseIntent {
113            product: self.product.into_product()?,
114            offer: self
115                .offer
116                .map(SubscriptionOfferPayload::into_subscription_offer),
117        })
118    }
119}