1use core::ffi::c_void;
2use core::ptr;
3
4use serde::{Deserialize, Serialize};
5
6use crate::app_store::AppStore;
7use crate::error::StoreKitError;
8use crate::ffi;
9use crate::private::{cstring_from_str, error_from_status, json_cstring, parse_json_ptr};
10use crate::product::ProductType;
11use crate::purchase_option::{PurchaseResult, PurchaseResultPayload};
12use crate::renewal_info::RenewalInfo;
13use crate::subscription::{SubscriptionPeriod, SubscriptionPeriodPayload};
14use crate::transaction::{Transaction, TransactionStream};
15use crate::verification_result::VerificationResult;
16use crate::window::NSWindowHandle;
17
18#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
19#[serde(tag = "kind", rename_all = "camelCase")]
20pub enum AppStoreMerchandisingKind {
21 SubscriptionBundle {
22 #[serde(rename = "groupID")]
23 group_id: String,
24 },
25}
26
27impl AppStoreMerchandisingKind {
28 pub fn subscription_bundle(group_id: impl Into<String>) -> Self {
29 Self::SubscriptionBundle {
30 group_id: group_id.into(),
31 }
32 }
33}
34
35#[derive(Debug)]
36#[allow(clippy::large_enum_variant)]
37pub enum AppStoreMerchandisingPresentationResult {
38 Dismissed,
39 PurchaseCompleted(PurchaseResult),
40}
41
42impl AppStore {
43 pub fn age_rating_code() -> Result<Option<i64>, StoreKitError> {
44 let mut raw_value = 0_i64;
45 let mut has_value = 0;
46 let mut error_message = ptr::null_mut();
47 let status = unsafe {
48 ffi::sk_app_store_age_rating_code(
49 &mut raw_value,
50 &mut has_value,
51 &mut error_message,
52 )
53 };
54 if status == ffi::status::OK {
55 Ok((has_value != 0).then_some(raw_value))
56 } else {
57 Err(unsafe { error_from_status(status, error_message) })
58 }
59 }
60
61 pub fn present_merchandising(
62 kind: &AppStoreMerchandisingKind,
63 window: &NSWindowHandle,
64 ) -> Result<AppStoreMerchandisingPresentationResult, StoreKitError> {
65 let kind_json = json_cstring(kind, "App Store merchandising kind")?;
66 let mut transaction_handle: *mut c_void = ptr::null_mut();
67 let mut result_json = ptr::null_mut();
68 let mut error_message = ptr::null_mut();
69 let status = unsafe {
70 ffi::sk_app_store_present_merchandising(
71 kind_json.as_ptr(),
72 window.as_raw(),
73 &mut transaction_handle,
74 &mut result_json,
75 &mut error_message,
76 )
77 };
78 if status != ffi::status::OK {
79 return Err(unsafe { error_from_status(status, error_message) });
80 }
81 let payload = unsafe {
82 parse_json_ptr::<AppStoreMerchandisingPresentationResultPayload>(
83 result_json,
84 "App Store merchandising presentation result",
85 )
86 }?;
87 payload.into_result(transaction_handle)
88 }
89}
90
91#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
92#[serde(tag = "kind", rename_all = "camelCase")]
93pub enum AdvancedCommercePurchaseOption {
94 OnStorefrontChange {
95 #[serde(rename = "shouldContinuePurchase")]
96 should_continue_purchase: bool,
97 },
98}
99
100#[derive(Debug, Clone, PartialEq, Eq)]
101pub struct AdvancedCommerceProduct {
102 pub id: String,
103 pub product_type: ProductType,
104}
105
106impl AdvancedCommerceProduct {
107 pub fn new(id: &str) -> Result<Self, StoreKitError> {
108 let product_id = cstring_from_str(id, "advanced commerce product id")?;
109 let mut product_json = ptr::null_mut();
110 let mut error_message = ptr::null_mut();
111 let status = unsafe {
112 ffi::sk_advanced_commerce_product_json(
113 product_id.as_ptr(),
114 &mut product_json,
115 &mut error_message,
116 )
117 };
118 if status != ffi::status::OK {
119 return Err(unsafe { error_from_status(status, error_message) });
120 }
121 let payload = unsafe {
122 parse_json_ptr::<AdvancedCommerceProductPayload>(
123 product_json,
124 "advanced commerce product",
125 )
126 }?;
127 Ok(payload.into_product())
128 }
129
130 pub fn purchase_in_window(
131 &self,
132 compact_jws: &str,
133 window: &NSWindowHandle,
134 options: &[AdvancedCommercePurchaseOption],
135 ) -> Result<PurchaseResult, StoreKitError> {
136 let product_id = cstring_from_str(&self.id, "advanced commerce product id")?;
137 let compact_jws = cstring_from_str(compact_jws, "advanced commerce compact JWS")?;
138 let options_json = json_cstring(options, "advanced commerce purchase options")?;
139 let mut transaction_handle: *mut c_void = ptr::null_mut();
140 let mut result_json = ptr::null_mut();
141 let mut error_message = ptr::null_mut();
142 let status = unsafe {
143 ffi::sk_advanced_commerce_product_purchase(
144 product_id.as_ptr(),
145 compact_jws.as_ptr(),
146 window.as_raw(),
147 options_json.as_ptr(),
148 &mut transaction_handle,
149 &mut result_json,
150 &mut error_message,
151 )
152 };
153 if status != ffi::status::OK {
154 return Err(unsafe { error_from_status(status, error_message) });
155 }
156
157 let payload = unsafe {
158 parse_json_ptr::<PurchaseResultPayload>(result_json, "advanced commerce purchase result")
159 };
160 match payload {
161 Ok(payload) => payload.into_purchase_result(transaction_handle),
162 Err(error) => {
163 if !transaction_handle.is_null() {
164 unsafe { ffi::sk_transaction_release(transaction_handle) };
165 }
166 Err(error)
167 }
168 }
169 }
170
171 pub fn latest_transaction(&self) -> Result<Option<VerificationResult<Transaction>>, StoreKitError> {
172 Transaction::latest_for(&self.id)
173 }
174
175 pub fn all_transactions(&self) -> Result<TransactionStream, StoreKitError> {
176 Transaction::all_for(&self.id)
177 }
178
179 pub fn current_entitlements(&self) -> Result<TransactionStream, StoreKitError> {
180 Transaction::current_entitlements_for(&self.id)
181 }
182}
183
184#[derive(Debug, Clone, PartialEq, Eq)]
185pub struct TransactionAdvancedCommerceInfo {
186 pub request_reference_id: String,
187 pub estimated_tax: String,
188 pub tax_rate: String,
189 pub tax_code: String,
190 pub tax_exclusive_price: String,
191 pub description: Option<String>,
192 pub display_name: Option<String>,
193 pub period: Option<SubscriptionPeriod>,
194 pub items: Vec<TransactionAdvancedCommerceItem>,
195}
196
197#[derive(Debug, Clone, PartialEq, Eq)]
198pub struct TransactionAdvancedCommerceItem {
199 pub details: TransactionAdvancedCommerceItemDetails,
200 pub refunds: Option<Vec<TransactionAdvancedCommerceRefund>>,
201 pub revocation_date: Option<String>,
202}
203
204#[derive(Debug, Clone, PartialEq, Eq)]
205pub struct TransactionAdvancedCommerceItemDetails {
206 pub sku: String,
207 pub display_name: String,
208 pub description: String,
209 pub offer: Option<TransactionAdvancedCommerceOffer>,
210 pub price: String,
211}
212
213#[derive(Debug, Clone, PartialEq, Eq)]
214pub struct TransactionAdvancedCommerceOffer {
215 pub price: String,
216 pub period: SubscriptionPeriod,
217 pub period_count: i64,
218 pub reason: TransactionAdvancedCommerceOfferReason,
219}
220
221#[derive(Debug, Clone, PartialEq, Eq)]
222pub enum TransactionAdvancedCommerceOfferReason {
223 Acquisition,
224 Retention,
225 WinBack,
226 Unknown(String),
227}
228
229impl TransactionAdvancedCommerceOfferReason {
230 pub fn as_str(&self) -> &str {
231 match self {
232 Self::Acquisition => "acquisition",
233 Self::Retention => "retention",
234 Self::WinBack => "winBack",
235 Self::Unknown(value) => value.as_str(),
236 }
237 }
238
239 fn from_raw(raw: String) -> Self {
240 match raw.as_str() {
241 "acquisition" => Self::Acquisition,
242 "retention" => Self::Retention,
243 "winBack" => Self::WinBack,
244 _ => Self::Unknown(raw),
245 }
246 }
247}
248
249#[derive(Debug, Clone, PartialEq, Eq)]
250pub struct TransactionAdvancedCommerceRefund {
251 pub reason: TransactionAdvancedCommerceRefundReason,
252 pub refund_type: TransactionAdvancedCommerceRefundType,
253 pub date: String,
254 pub amount: String,
255}
256
257#[derive(Debug, Clone, PartialEq, Eq)]
258pub enum TransactionAdvancedCommerceRefundReason {
259 Legal,
260 ModifyItems,
261 Unintended,
262 Unfulfilled,
263 Unsatisfied,
264 Other,
265 Unknown(String),
266}
267
268impl TransactionAdvancedCommerceRefundReason {
269 pub fn as_str(&self) -> &str {
270 match self {
271 Self::Legal => "legal",
272 Self::ModifyItems => "modifyItems",
273 Self::Unintended => "unintended",
274 Self::Unfulfilled => "unfulfilled",
275 Self::Unsatisfied => "unsatisfied",
276 Self::Other => "other",
277 Self::Unknown(value) => value.as_str(),
278 }
279 }
280
281 fn from_raw(raw: String) -> Self {
282 match raw.as_str() {
283 "legal" => Self::Legal,
284 "modifyItems" => Self::ModifyItems,
285 "unintended" => Self::Unintended,
286 "unfulfilled" => Self::Unfulfilled,
287 "unsatisfied" => Self::Unsatisfied,
288 "other" => Self::Other,
289 _ => Self::Unknown(raw),
290 }
291 }
292}
293
294#[derive(Debug, Clone, PartialEq, Eq)]
295pub enum TransactionAdvancedCommerceRefundType {
296 Custom,
297 ProRated,
298 Full,
299 Unknown(String),
300}
301
302impl TransactionAdvancedCommerceRefundType {
303 pub fn as_str(&self) -> &str {
304 match self {
305 Self::Custom => "custom",
306 Self::ProRated => "proRated",
307 Self::Full => "full",
308 Self::Unknown(value) => value.as_str(),
309 }
310 }
311
312 fn from_raw(raw: String) -> Self {
313 match raw.as_str() {
314 "custom" => Self::Custom,
315 "proRated" => Self::ProRated,
316 "full" => Self::Full,
317 _ => Self::Unknown(raw),
318 }
319 }
320}
321
322#[derive(Debug, Clone, PartialEq, Eq)]
323pub struct RenewalInfoAdvancedCommerceInfo {
324 pub consistency_token: String,
325 pub request_reference_id: String,
326 pub tax_code: String,
327 pub description: String,
328 pub display_name: String,
329 pub period: SubscriptionPeriod,
330 pub items: Vec<RenewalInfoAdvancedCommerceItem>,
331}
332
333#[derive(Debug, Clone, PartialEq, Eq)]
334pub struct RenewalInfoAdvancedCommerceItem {
335 pub details: TransactionAdvancedCommerceItemDetails,
336 pub price_increase_info: Option<RenewalInfoAdvancedCommercePriceIncreaseInfo>,
337}
338
339#[derive(Debug, Clone, PartialEq, Eq)]
340pub struct RenewalInfoAdvancedCommercePriceIncreaseInfo {
341 pub status: RenewalInfoAdvancedCommercePriceIncreaseStatus,
342 pub price: String,
343 pub dependent_skus: Vec<String>,
344}
345
346#[derive(Debug, Clone, PartialEq, Eq)]
347pub enum RenewalInfoAdvancedCommercePriceIncreaseStatus {
348 Pending,
349 Accepted,
350 Scheduled,
351 Unknown(String),
352}
353
354impl RenewalInfoAdvancedCommercePriceIncreaseStatus {
355 pub fn as_str(&self) -> &str {
356 match self {
357 Self::Pending => "pending",
358 Self::Accepted => "accepted",
359 Self::Scheduled => "scheduled",
360 Self::Unknown(value) => value.as_str(),
361 }
362 }
363
364 fn from_raw(raw: String) -> Self {
365 match raw.as_str() {
366 "pending" => Self::Pending,
367 "accepted" => Self::Accepted,
368 "scheduled" => Self::Scheduled,
369 _ => Self::Unknown(raw),
370 }
371 }
372}
373
374impl VerificationResult<Transaction> {
375 pub fn advanced_commerce_info(&self) -> Result<Option<TransactionAdvancedCommerceInfo>, StoreKitError> {
376 parse_transaction_advanced_commerce_info_payload(&self.metadata().payload_data)
377 }
378}
379
380impl VerificationResult<RenewalInfo> {
381 pub fn advanced_commerce_info(&self) -> Result<Option<RenewalInfoAdvancedCommerceInfo>, StoreKitError> {
382 parse_renewal_advanced_commerce_info_payload(&self.metadata().payload_data)
383 }
384}
385
386#[derive(Debug, Deserialize)]
387pub(crate) struct TransactionAdvancedCommerceInfoPayload {
388 #[serde(rename = "requestReferenceID")]
389 request_reference_id: String,
390 #[serde(rename = "estimatedTax")]
391 estimated_tax: String,
392 #[serde(rename = "taxRate")]
393 tax_rate: String,
394 #[serde(rename = "taxCode")]
395 tax_code: String,
396 #[serde(rename = "taxExclusivePrice")]
397 tax_exclusive_price: String,
398 description: Option<String>,
399 #[serde(rename = "displayName")]
400 display_name: Option<String>,
401 period: Option<SubscriptionPeriodPayload>,
402 items: Vec<TransactionAdvancedCommerceItemPayload>,
403}
404
405impl TransactionAdvancedCommerceInfoPayload {
406 pub(crate) fn into_transaction_advanced_commerce_info(self) -> TransactionAdvancedCommerceInfo {
407 TransactionAdvancedCommerceInfo {
408 request_reference_id: self.request_reference_id,
409 estimated_tax: self.estimated_tax,
410 tax_rate: self.tax_rate,
411 tax_code: self.tax_code,
412 tax_exclusive_price: self.tax_exclusive_price,
413 description: self.description,
414 display_name: self.display_name,
415 period: self.period.map(SubscriptionPeriodPayload::into_subscription_period),
416 items: self
417 .items
418 .into_iter()
419 .map(TransactionAdvancedCommerceItemPayload::into_transaction_advanced_commerce_item)
420 .collect(),
421 }
422 }
423}
424
425#[derive(Debug, Deserialize)]
426struct TransactionAdvancedCommerceItemPayload {
427 details: TransactionAdvancedCommerceItemDetailsPayload,
428 refunds: Option<Vec<TransactionAdvancedCommerceRefundPayload>>,
429 #[serde(rename = "revocationDate")]
430 revocation_date: Option<String>,
431}
432
433impl TransactionAdvancedCommerceItemPayload {
434 fn into_transaction_advanced_commerce_item(self) -> TransactionAdvancedCommerceItem {
435 TransactionAdvancedCommerceItem {
436 details: self.details.into_transaction_advanced_commerce_item_details(),
437 refunds: self.refunds.map(|refunds| {
438 refunds
439 .into_iter()
440 .map(TransactionAdvancedCommerceRefundPayload::into_transaction_advanced_commerce_refund)
441 .collect()
442 }),
443 revocation_date: self.revocation_date,
444 }
445 }
446}
447
448#[derive(Debug, Deserialize)]
449struct TransactionAdvancedCommerceItemDetailsPayload {
450 sku: String,
451 #[serde(rename = "displayName")]
452 display_name: String,
453 description: String,
454 offer: Option<TransactionAdvancedCommerceOfferPayload>,
455 price: String,
456}
457
458impl TransactionAdvancedCommerceItemDetailsPayload {
459 fn into_transaction_advanced_commerce_item_details(self) -> TransactionAdvancedCommerceItemDetails {
460 TransactionAdvancedCommerceItemDetails {
461 sku: self.sku,
462 display_name: self.display_name,
463 description: self.description,
464 offer: self.offer.map(TransactionAdvancedCommerceOfferPayload::into_transaction_advanced_commerce_offer),
465 price: self.price,
466 }
467 }
468}
469
470#[derive(Debug, Deserialize)]
471struct TransactionAdvancedCommerceOfferPayload {
472 price: String,
473 period: SubscriptionPeriodPayload,
474 #[serde(rename = "periodCount")]
475 period_count: i64,
476 reason: String,
477}
478
479impl TransactionAdvancedCommerceOfferPayload {
480 fn into_transaction_advanced_commerce_offer(self) -> TransactionAdvancedCommerceOffer {
481 TransactionAdvancedCommerceOffer {
482 price: self.price,
483 period: self.period.into_subscription_period(),
484 period_count: self.period_count,
485 reason: TransactionAdvancedCommerceOfferReason::from_raw(self.reason),
486 }
487 }
488}
489
490#[derive(Debug, Deserialize)]
491struct TransactionAdvancedCommerceRefundPayload {
492 reason: String,
493 #[serde(rename = "type")]
494 refund_type: String,
495 date: String,
496 amount: String,
497}
498
499impl TransactionAdvancedCommerceRefundPayload {
500 fn into_transaction_advanced_commerce_refund(self) -> TransactionAdvancedCommerceRefund {
501 TransactionAdvancedCommerceRefund {
502 reason: TransactionAdvancedCommerceRefundReason::from_raw(self.reason),
503 refund_type: TransactionAdvancedCommerceRefundType::from_raw(self.refund_type),
504 date: self.date,
505 amount: self.amount,
506 }
507 }
508}
509
510#[derive(Debug, Deserialize)]
511struct RenewalSignedPayload {
512 #[serde(rename = "advancedCommerceInfo")]
513 advanced_commerce_info: Option<RenewalInfoAdvancedCommerceInfoPayload>,
514}
515
516#[derive(Debug, Deserialize)]
517struct RenewalInfoAdvancedCommerceInfoPayload {
518 #[serde(rename = "consistencyToken")]
519 consistency_token: String,
520 #[serde(rename = "requestReferenceID")]
521 request_reference_id: String,
522 #[serde(rename = "taxCode")]
523 tax_code: String,
524 description: String,
525 #[serde(rename = "displayName")]
526 display_name: String,
527 period: SubscriptionPeriodPayload,
528 items: Vec<RenewalInfoAdvancedCommerceItemPayload>,
529}
530
531impl RenewalInfoAdvancedCommerceInfoPayload {
532 fn into_renewal_info_advanced_commerce_info(self) -> RenewalInfoAdvancedCommerceInfo {
533 RenewalInfoAdvancedCommerceInfo {
534 consistency_token: self.consistency_token,
535 request_reference_id: self.request_reference_id,
536 tax_code: self.tax_code,
537 description: self.description,
538 display_name: self.display_name,
539 period: self.period.into_subscription_period(),
540 items: self
541 .items
542 .into_iter()
543 .map(RenewalInfoAdvancedCommerceItemPayload::into_renewal_info_advanced_commerce_item)
544 .collect(),
545 }
546 }
547}
548
549#[derive(Debug, Deserialize)]
550struct RenewalInfoAdvancedCommerceItemPayload {
551 details: TransactionAdvancedCommerceItemDetailsPayload,
552 #[serde(rename = "priceIncreaseInfo")]
553 price_increase_info: Option<RenewalInfoAdvancedCommercePriceIncreaseInfoPayload>,
554}
555
556impl RenewalInfoAdvancedCommerceItemPayload {
557 fn into_renewal_info_advanced_commerce_item(self) -> RenewalInfoAdvancedCommerceItem {
558 RenewalInfoAdvancedCommerceItem {
559 details: self.details.into_transaction_advanced_commerce_item_details(),
560 price_increase_info: self.price_increase_info.map(
561 RenewalInfoAdvancedCommercePriceIncreaseInfoPayload::into_renewal_info_advanced_commerce_price_increase_info,
562 ),
563 }
564 }
565}
566
567#[derive(Debug, Deserialize)]
568struct RenewalInfoAdvancedCommercePriceIncreaseInfoPayload {
569 status: String,
570 price: String,
571 #[serde(rename = "dependentSKUs")]
572 dependent_skus: Vec<String>,
573}
574
575impl RenewalInfoAdvancedCommercePriceIncreaseInfoPayload {
576 fn into_renewal_info_advanced_commerce_price_increase_info(self) -> RenewalInfoAdvancedCommercePriceIncreaseInfo {
577 RenewalInfoAdvancedCommercePriceIncreaseInfo {
578 status: RenewalInfoAdvancedCommercePriceIncreaseStatus::from_raw(self.status),
579 price: self.price,
580 dependent_skus: self.dependent_skus,
581 }
582 }
583}
584
585#[derive(Debug, Deserialize)]
586struct AdvancedCommerceProductPayload {
587 id: String,
588 #[serde(rename = "type")]
589 product_type: String,
590}
591
592impl AdvancedCommerceProductPayload {
593 fn into_product(self) -> AdvancedCommerceProduct {
594 AdvancedCommerceProduct {
595 id: self.id,
596 product_type: ProductType::from_raw(self.product_type),
597 }
598 }
599}
600
601#[derive(Debug, Deserialize)]
602#[allow(clippy::unsafe_derive_deserialize)]
603pub(crate) struct AppStoreMerchandisingPresentationResultPayload {
604 kind: String,
605 #[serde(rename = "purchaseResult")]
606 purchase_result: Option<PurchaseResultPayload>,
607}
608
609impl AppStoreMerchandisingPresentationResultPayload {
610 pub(crate) fn into_result(
611 self,
612 transaction_handle: *mut c_void,
613 ) -> Result<AppStoreMerchandisingPresentationResult, StoreKitError> {
614 match self.kind.as_str() {
615 "dismissed" => {
616 if !transaction_handle.is_null() {
617 unsafe { ffi::sk_transaction_release(transaction_handle) };
618 }
619 Ok(AppStoreMerchandisingPresentationResult::Dismissed)
620 }
621 "purchaseCompleted" => {
622 let purchase_result = self.purchase_result.ok_or_else(|| {
623 StoreKitError::Unknown(
624 "App Store merchandising reported a purchase completion without a purchase result"
625 .to_owned(),
626 )
627 })?;
628 Ok(AppStoreMerchandisingPresentationResult::PurchaseCompleted(
629 purchase_result.into_purchase_result(transaction_handle)?,
630 ))
631 }
632 other => {
633 if !transaction_handle.is_null() {
634 unsafe { ffi::sk_transaction_release(transaction_handle) };
635 }
636 Err(StoreKitError::Unknown(format!(
637 "unknown App Store merchandising presentation result kind '{other}'"
638 )))
639 }
640 }
641 }
642}
643
644#[derive(Debug, Deserialize)]
645struct TransactionSignedPayload {
646 #[serde(rename = "advancedCommerceInfo")]
647 advanced_commerce_info: Option<TransactionAdvancedCommerceInfoPayload>,
648}
649
650fn parse_transaction_advanced_commerce_info_payload(
651 payload_data: &[u8],
652) -> Result<Option<TransactionAdvancedCommerceInfo>, StoreKitError> {
653 let payload = serde_json::from_slice::<TransactionSignedPayload>(payload_data).map_err(|error| {
654 StoreKitError::InvalidArgument(format!(
655 "failed to parse signed transaction payload JSON: {error}"
656 ))
657 })?;
658 Ok(payload
659 .advanced_commerce_info
660 .map(TransactionAdvancedCommerceInfoPayload::into_transaction_advanced_commerce_info))
661}
662
663fn parse_renewal_advanced_commerce_info_payload(
664 payload_data: &[u8],
665) -> Result<Option<RenewalInfoAdvancedCommerceInfo>, StoreKitError> {
666 let payload = serde_json::from_slice::<RenewalSignedPayload>(payload_data).map_err(|error| {
667 StoreKitError::InvalidArgument(format!(
668 "failed to parse signed renewal payload JSON: {error}"
669 ))
670 })?;
671 Ok(payload
672 .advanced_commerce_info
673 .map(RenewalInfoAdvancedCommerceInfoPayload::into_renewal_info_advanced_commerce_info))
674}