Skip to main content

storekit/
product_helpers.rs

1use core::ffi::c_void;
2use core::ptr;
3
4use serde::Deserialize;
5
6use crate::error::StoreKitError;
7use crate::ffi;
8use crate::private::{cstring_from_str, error_from_status, parse_json_ptr, take_string};
9use crate::product::{Product, ProductType, PurchaseOption, PurchaseResult};
10use crate::purchase_option::PurchaseResultPayload;
11use crate::renewal_info::{ExpirationReason, PriceIncreaseStatus};
12use crate::renewal_state::RenewalState;
13use crate::subscription::{
14    SubscriptionOfferType, SubscriptionPaymentMode, SubscriptionPeriod, SubscriptionPeriodUnit,
15};
16use crate::transaction::{OfferType, OwnershipType, RevocationReason};
17use crate::window::NSWindowHandle;
18
19#[derive(Debug, Clone, PartialEq, Eq)]
20/// Carries localized formatting derived from `StoreKit.Product`.
21pub struct ProductFormatting {
22    /// Formatted price string returned by `StoreKit`.
23    pub formatted_price: String,
24    /// Formatted subscription period string returned by `StoreKit`.
25    pub formatted_subscription_period: Option<String>,
26    /// Formatted subscription period unit string returned by `StoreKit`.
27    pub formatted_subscription_period_unit: Option<String>,
28}
29
30impl Product {
31    /// Calls the `StoreKit` purchase API while presenting UI in the supplied `NSWindow`.
32    pub fn purchase_in_window(
33        &self,
34        window: &NSWindowHandle,
35        options: &[PurchaseOption],
36    ) -> Result<PurchaseResult, StoreKitError> {
37        let product_id = cstring_from_str(&self.id, "product id")?;
38        let options_json = crate::private::json_cstring(options, "purchase options")?;
39        let mut transaction_handle: *mut c_void = ptr::null_mut();
40        let mut result_json = ptr::null_mut();
41        let mut error_message = ptr::null_mut();
42        let status = unsafe {
43            ffi::sk_product_purchase_in_window(
44                product_id.as_ptr(),
45                window.as_raw(),
46                options_json.as_ptr(),
47                &mut transaction_handle,
48                &mut result_json,
49                &mut error_message,
50            )
51        };
52        if status != ffi::status::OK {
53            return Err(unsafe { error_from_status(status, error_message) });
54        }
55
56        let payload =
57            unsafe { parse_json_ptr::<PurchaseResultPayload>(result_json, "purchase result") };
58        match payload {
59            Ok(payload) => payload.into_purchase_result(transaction_handle),
60            Err(error) => {
61                if !transaction_handle.is_null() {
62                    unsafe { ffi::sk_transaction_release(transaction_handle) };
63                }
64                Err(error)
65            }
66        }
67    }
68
69    /// Fetches `StoreKit`-provided formatting for this product.
70    pub fn formatting(&self) -> Result<ProductFormatting, StoreKitError> {
71        let product_id = cstring_from_str(&self.id, "product id")?;
72        let mut formatting_json = ptr::null_mut();
73        let mut error_message = ptr::null_mut();
74        let status = unsafe {
75            ffi::sk_product_formatting_json(
76                product_id.as_ptr(),
77                &mut formatting_json,
78                &mut error_message,
79            )
80        };
81        if status != ffi::status::OK {
82            return Err(unsafe { error_from_status(status, error_message) });
83        }
84        let payload = unsafe {
85            parse_json_ptr::<ProductFormattingPayload>(formatting_json, "product formatting")
86        }?;
87        Ok(payload.into_product_formatting())
88    }
89
90    /// Returns the `StoreKit`-formatted price for this product.
91    pub fn formatted_price(&self) -> Result<String, StoreKitError> {
92        self.formatting()
93            .map(|formatting| formatting.formatted_price)
94    }
95
96    /// Returns the `StoreKit`-formatted subscription period for this product, if available.
97    pub fn formatted_subscription_period(&self) -> Result<Option<String>, StoreKitError> {
98        self.formatting()
99            .map(|formatting| formatting.formatted_subscription_period)
100    }
101
102    /// Returns the `StoreKit`-formatted subscription period unit for this product, if available.
103    pub fn formatted_subscription_period_unit(&self) -> Result<Option<String>, StoreKitError> {
104        self.formatting()
105            .map(|formatting| formatting.formatted_subscription_period_unit)
106    }
107}
108
109impl SubscriptionPeriod {
110    /// Builds a `SubscriptionPeriod` matching a weekly `StoreKit` period.
111    pub const fn weekly() -> Self {
112        Self {
113            unit: SubscriptionPeriodUnit::Week,
114            value: 1,
115        }
116    }
117
118    /// Builds a `SubscriptionPeriod` matching a monthly `StoreKit` period.
119    pub const fn monthly() -> Self {
120        Self {
121            unit: SubscriptionPeriodUnit::Month,
122            value: 1,
123        }
124    }
125
126    /// Builds a `SubscriptionPeriod` matching a yearly `StoreKit` period.
127    pub const fn yearly() -> Self {
128        Self {
129            unit: SubscriptionPeriodUnit::Year,
130            value: 1,
131        }
132    }
133
134    /// Builds a `SubscriptionPeriod` matching a three-day `StoreKit` period.
135    pub const fn every_three_days() -> Self {
136        Self {
137            unit: SubscriptionPeriodUnit::Day,
138            value: 3,
139        }
140    }
141
142    /// Builds a `SubscriptionPeriod` matching a two-week `StoreKit` period.
143    pub const fn every_two_weeks() -> Self {
144        Self {
145            unit: SubscriptionPeriodUnit::Week,
146            value: 2,
147        }
148    }
149
150    /// Builds a `SubscriptionPeriod` matching a two-month `StoreKit` period.
151    pub const fn every_two_months() -> Self {
152        Self {
153            unit: SubscriptionPeriodUnit::Month,
154            value: 2,
155        }
156    }
157
158    /// Builds a `SubscriptionPeriod` matching a three-month `StoreKit` period.
159    pub const fn every_three_months() -> Self {
160        Self {
161            unit: SubscriptionPeriodUnit::Month,
162            value: 3,
163        }
164    }
165
166    /// Builds a `SubscriptionPeriod` matching a six-month `StoreKit` period.
167    pub const fn every_six_months() -> Self {
168        Self {
169            unit: SubscriptionPeriodUnit::Month,
170            value: 6,
171        }
172    }
173}
174
175impl ProductType {
176    /// Returns the localized `StoreKit` description for this product type.
177    pub fn localized_description(&self) -> Result<String, StoreKitError> {
178        localized_description("productType", self.as_str())
179    }
180}
181
182impl RenewalState {
183    /// Returns the localized `StoreKit` description for this renewal state.
184    pub fn localized_description(&self) -> Result<String, StoreKitError> {
185        localized_description("renewalState", self.as_str())
186    }
187}
188
189impl ExpirationReason {
190    /// Returns the localized `StoreKit` description for this expiration reason.
191    pub fn localized_description(&self) -> Result<String, StoreKitError> {
192        localized_description("expirationReason", self.as_str())
193    }
194}
195
196impl PriceIncreaseStatus {
197    /// Returns the localized `StoreKit` description for this price increase status.
198    pub fn localized_description(&self) -> Result<String, StoreKitError> {
199        localized_description("priceIncreaseStatus", self.as_str())
200    }
201}
202
203impl SubscriptionOfferType {
204    /// Returns the localized `StoreKit` description for this subscription offer type.
205    pub fn localized_description(&self) -> Result<String, StoreKitError> {
206        localized_description("subscriptionOfferType", self.as_str())
207    }
208}
209
210impl OfferType {
211    /// Returns the localized `StoreKit` description for this transaction offer type.
212    pub fn localized_description(&self) -> Result<String, StoreKitError> {
213        localized_description("transactionOfferType", self.as_str())
214    }
215}
216
217impl SubscriptionPaymentMode {
218    /// Returns the localized `StoreKit` description for this subscription payment mode.
219    pub fn localized_description(&self) -> Result<String, StoreKitError> {
220        localized_description("subscriptionPaymentMode", self.as_str())
221    }
222}
223
224impl SubscriptionPeriodUnit {
225    /// Returns the localized `StoreKit` description for this subscription period unit.
226    pub fn localized_description(&self) -> Result<String, StoreKitError> {
227        localized_description("subscriptionPeriodUnit", self.as_str())
228    }
229}
230
231impl RevocationReason {
232    /// Returns the localized `StoreKit` description for this revocation reason.
233    pub fn localized_description(&self) -> Result<String, StoreKitError> {
234        localized_description("revocationReason", self.as_str())
235    }
236}
237
238impl OwnershipType {
239    /// Returns the localized `StoreKit` description for this ownership type.
240    pub fn localized_description(&self) -> Result<String, StoreKitError> {
241        let raw = match self {
242            Self::Purchased => "purchased",
243            Self::FamilyShared => "familyShared",
244            Self::Unknown(value) => value.as_str(),
245        };
246        localized_description("ownershipType", raw)
247    }
248}
249
250fn localized_description(kind: &str, raw_value: &str) -> Result<String, StoreKitError> {
251    let kind = cstring_from_str(kind, "localized description kind")?;
252    let raw_value = cstring_from_str(raw_value, "localized description raw value")?;
253    let mut localized = ptr::null_mut();
254    let mut error_message = ptr::null_mut();
255    let status = unsafe {
256        ffi::sk_localized_description(
257            kind.as_ptr(),
258            raw_value.as_ptr(),
259            &mut localized,
260            &mut error_message,
261        )
262    };
263    if status != ffi::status::OK {
264        return Err(unsafe { error_from_status(status, error_message) });
265    }
266    unsafe { take_string(localized) }
267        .ok_or_else(|| StoreKitError::Unknown("missing localized description payload".to_owned()))
268}
269
270#[derive(Debug, Deserialize)]
271struct ProductFormattingPayload {
272    #[serde(rename = "formattedPrice")]
273    price: String,
274    #[serde(rename = "formattedSubscriptionPeriod")]
275    subscription_period: Option<String>,
276    #[serde(rename = "formattedSubscriptionPeriodUnit")]
277    subscription_period_unit: Option<String>,
278}
279
280impl ProductFormattingPayload {
281    fn into_product_formatting(self) -> ProductFormatting {
282        ProductFormatting {
283            formatted_price: self.price,
284            formatted_subscription_period: self.subscription_period,
285            formatted_subscription_period_unit: self.subscription_period_unit,
286        }
287    }
288}