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 {
22 SubscriptionBundle {
24 #[serde(rename = "groupID")]
25 group_id: String,
27 },
28}
29
30impl AppStoreMerchandisingKind {
31 pub fn subscription_bundle(group_id: impl Into<String>) -> Self {
33 Self::SubscriptionBundle {
34 group_id: group_id.into(),
35 }
36 }
37}
38
39#[derive(Debug)]
40#[allow(clippy::large_enum_variant)]
41pub enum AppStoreMerchandisingPresentationResult {
43 Dismissed,
45 PurchaseCompleted(PurchaseResult),
47}
48
49impl AppStore {
50 pub fn age_rating_code() -> Result<Option<i64>, StoreKitError> {
52 let mut raw_value = 0_i64;
53 let mut has_value = 0;
54 let mut error_message = ptr::null_mut();
55 let status = unsafe {
56 ffi::sk_app_store_age_rating_code(&mut raw_value, &mut has_value, &mut error_message)
57 };
58 if status == ffi::status::OK {
59 Ok((has_value != 0).then_some(raw_value))
60 } else {
61 Err(unsafe { error_from_status(status, error_message) })
62 }
63 }
64
65 pub fn present_merchandising(
67 kind: &AppStoreMerchandisingKind,
68 window: &NSWindowHandle,
69 ) -> Result<AppStoreMerchandisingPresentationResult, StoreKitError> {
70 let kind_json = json_cstring(kind, "App Store merchandising kind")?;
71 let mut transaction_handle: *mut c_void = ptr::null_mut();
72 let mut result_json = ptr::null_mut();
73 let mut error_message = ptr::null_mut();
74 let status = unsafe {
75 ffi::sk_app_store_present_merchandising(
76 kind_json.as_ptr(),
77 window.as_raw(),
78 &mut transaction_handle,
79 &mut result_json,
80 &mut error_message,
81 )
82 };
83 if status != ffi::status::OK {
84 return Err(unsafe { error_from_status(status, error_message) });
85 }
86 let payload = unsafe {
87 parse_json_ptr::<AppStoreMerchandisingPresentationResultPayload>(
88 result_json,
89 "App Store merchandising presentation result",
90 )
91 }?;
92 payload.into_result(transaction_handle)
93 }
94}
95
96#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
97#[serde(tag = "kind", rename_all = "camelCase")]
98pub enum AdvancedCommercePurchaseOption {
100 OnStorefrontChange {
102 #[serde(rename = "shouldContinuePurchase")]
103 should_continue_purchase: bool,
105 },
106}
107
108#[derive(Debug, Clone, PartialEq, Eq)]
109pub struct AdvancedCommerceProduct {
111 pub id: String,
113 pub product_type: ProductType,
115}
116
117impl AdvancedCommerceProduct {
118 pub fn new(id: &str) -> Result<Self, StoreKitError> {
120 let product_id = cstring_from_str(id, "advanced commerce product id")?;
121 let mut product_json = ptr::null_mut();
122 let mut error_message = ptr::null_mut();
123 let status = unsafe {
124 ffi::sk_advanced_commerce_product_json(
125 product_id.as_ptr(),
126 &mut product_json,
127 &mut error_message,
128 )
129 };
130 if status != ffi::status::OK {
131 return Err(unsafe { error_from_status(status, error_message) });
132 }
133 let payload = unsafe {
134 parse_json_ptr::<AdvancedCommerceProductPayload>(
135 product_json,
136 "advanced commerce product",
137 )
138 }?;
139 Ok(payload.into_product())
140 }
141
142 pub fn purchase_in_window(
144 &self,
145 compact_jws: &str,
146 window: &NSWindowHandle,
147 options: &[AdvancedCommercePurchaseOption],
148 ) -> Result<PurchaseResult, StoreKitError> {
149 let product_id = cstring_from_str(&self.id, "advanced commerce product id")?;
150 let compact_jws = cstring_from_str(compact_jws, "advanced commerce compact JWS")?;
151 let options_json = json_cstring(options, "advanced commerce purchase options")?;
152 let mut transaction_handle: *mut c_void = ptr::null_mut();
153 let mut result_json = ptr::null_mut();
154 let mut error_message = ptr::null_mut();
155 let status = unsafe {
156 ffi::sk_advanced_commerce_product_purchase(
157 product_id.as_ptr(),
158 compact_jws.as_ptr(),
159 window.as_raw(),
160 options_json.as_ptr(),
161 &mut transaction_handle,
162 &mut result_json,
163 &mut error_message,
164 )
165 };
166 if status != ffi::status::OK {
167 return Err(unsafe { error_from_status(status, error_message) });
168 }
169
170 let payload = unsafe {
171 parse_json_ptr::<PurchaseResultPayload>(
172 result_json,
173 "advanced commerce purchase result",
174 )
175 };
176 match payload {
177 Ok(payload) => payload.into_purchase_result(transaction_handle),
178 Err(error) => {
179 if !transaction_handle.is_null() {
180 unsafe { ffi::sk_transaction_release(transaction_handle) };
181 }
182 Err(error)
183 }
184 }
185 }
186
187 pub fn latest_transaction(
189 &self,
190 ) -> Result<Option<VerificationResult<Transaction>>, StoreKitError> {
191 Transaction::latest_for(&self.id)
192 }
193
194 pub fn all_transactions(&self) -> Result<TransactionStream, StoreKitError> {
196 Transaction::all_for(&self.id)
197 }
198
199 pub fn current_entitlements(&self) -> Result<TransactionStream, StoreKitError> {
201 Transaction::current_entitlements_for(&self.id)
202 }
203}
204
205#[derive(Debug, Clone, PartialEq, Eq)]
206pub struct TransactionAdvancedCommerceInfo {
208 pub request_reference_id: String,
210 pub estimated_tax: String,
212 pub tax_rate: String,
214 pub tax_code: String,
216 pub tax_exclusive_price: String,
218 pub description: Option<String>,
220 pub display_name: Option<String>,
222 pub period: Option<SubscriptionPeriod>,
224 pub items: Vec<TransactionAdvancedCommerceItem>,
226}
227
228#[derive(Debug, Clone, PartialEq, Eq)]
229pub struct TransactionAdvancedCommerceItem {
231 pub details: TransactionAdvancedCommerceItemDetails,
233 pub refunds: Option<Vec<TransactionAdvancedCommerceRefund>>,
235 pub revocation_date: Option<String>,
237}
238
239#[derive(Debug, Clone, PartialEq, Eq)]
240pub struct TransactionAdvancedCommerceItemDetails {
242 pub sku: String,
244 pub display_name: String,
246 pub description: String,
248 pub offer: Option<TransactionAdvancedCommerceOffer>,
250 pub price: String,
252}
253
254#[derive(Debug, Clone, PartialEq, Eq)]
255pub struct TransactionAdvancedCommerceOffer {
257 pub price: String,
259 pub period: SubscriptionPeriod,
261 pub period_count: i64,
263 pub reason: TransactionAdvancedCommerceOfferReason,
265}
266
267#[derive(Debug, Clone, PartialEq, Eq)]
268pub enum TransactionAdvancedCommerceOfferReason {
270 Acquisition,
272 Retention,
274 WinBack,
276 Unknown(String),
278}
279
280impl TransactionAdvancedCommerceOfferReason {
281 pub fn as_str(&self) -> &str {
283 match self {
284 Self::Acquisition => "acquisition",
285 Self::Retention => "retention",
286 Self::WinBack => "winBack",
287 Self::Unknown(value) => value.as_str(),
288 }
289 }
290
291 fn from_raw(raw: String) -> Self {
292 match raw.as_str() {
293 "acquisition" => Self::Acquisition,
294 "retention" => Self::Retention,
295 "winBack" => Self::WinBack,
296 _ => Self::Unknown(raw),
297 }
298 }
299}
300
301#[derive(Debug, Clone, PartialEq, Eq)]
302pub struct TransactionAdvancedCommerceRefund {
304 pub reason: TransactionAdvancedCommerceRefundReason,
306 pub refund_type: TransactionAdvancedCommerceRefundType,
308 pub date: String,
310 pub amount: String,
312}
313
314#[derive(Debug, Clone, PartialEq, Eq)]
315pub enum TransactionAdvancedCommerceRefundReason {
317 Legal,
319 ModifyItems,
321 Unintended,
323 Unfulfilled,
325 Unsatisfied,
327 Other,
329 Unknown(String),
331}
332
333impl TransactionAdvancedCommerceRefundReason {
334 pub fn as_str(&self) -> &str {
336 match self {
337 Self::Legal => "legal",
338 Self::ModifyItems => "modifyItems",
339 Self::Unintended => "unintended",
340 Self::Unfulfilled => "unfulfilled",
341 Self::Unsatisfied => "unsatisfied",
342 Self::Other => "other",
343 Self::Unknown(value) => value.as_str(),
344 }
345 }
346
347 fn from_raw(raw: String) -> Self {
348 match raw.as_str() {
349 "legal" => Self::Legal,
350 "modifyItems" => Self::ModifyItems,
351 "unintended" => Self::Unintended,
352 "unfulfilled" => Self::Unfulfilled,
353 "unsatisfied" => Self::Unsatisfied,
354 "other" => Self::Other,
355 _ => Self::Unknown(raw),
356 }
357 }
358}
359
360#[derive(Debug, Clone, PartialEq, Eq)]
361pub enum TransactionAdvancedCommerceRefundType {
363 Custom,
365 ProRated,
367 Full,
369 Unknown(String),
371}
372
373impl TransactionAdvancedCommerceRefundType {
374 pub fn as_str(&self) -> &str {
376 match self {
377 Self::Custom => "custom",
378 Self::ProRated => "proRated",
379 Self::Full => "full",
380 Self::Unknown(value) => value.as_str(),
381 }
382 }
383
384 fn from_raw(raw: String) -> Self {
385 match raw.as_str() {
386 "custom" => Self::Custom,
387 "proRated" => Self::ProRated,
388 "full" => Self::Full,
389 _ => Self::Unknown(raw),
390 }
391 }
392}
393
394#[derive(Debug, Clone, PartialEq, Eq)]
395pub struct RenewalInfoAdvancedCommerceInfo {
397 pub consistency_token: String,
399 pub request_reference_id: String,
401 pub tax_code: String,
403 pub description: String,
405 pub display_name: String,
407 pub period: SubscriptionPeriod,
409 pub items: Vec<RenewalInfoAdvancedCommerceItem>,
411}
412
413#[derive(Debug, Clone, PartialEq, Eq)]
414pub struct RenewalInfoAdvancedCommerceItem {
416 pub details: TransactionAdvancedCommerceItemDetails,
418 pub price_increase_info: Option<RenewalInfoAdvancedCommercePriceIncreaseInfo>,
420}
421
422#[derive(Debug, Clone, PartialEq, Eq)]
423pub struct RenewalInfoAdvancedCommercePriceIncreaseInfo {
425 pub status: RenewalInfoAdvancedCommercePriceIncreaseStatus,
427 pub price: String,
429 pub dependent_skus: Vec<String>,
431}
432
433#[derive(Debug, Clone, PartialEq, Eq)]
434pub enum RenewalInfoAdvancedCommercePriceIncreaseStatus {
436 Pending,
438 Accepted,
440 Scheduled,
442 Unknown(String),
444}
445
446impl RenewalInfoAdvancedCommercePriceIncreaseStatus {
447 pub fn as_str(&self) -> &str {
449 match self {
450 Self::Pending => "pending",
451 Self::Accepted => "accepted",
452 Self::Scheduled => "scheduled",
453 Self::Unknown(value) => value.as_str(),
454 }
455 }
456
457 fn from_raw(raw: String) -> Self {
458 match raw.as_str() {
459 "pending" => Self::Pending,
460 "accepted" => Self::Accepted,
461 "scheduled" => Self::Scheduled,
462 _ => Self::Unknown(raw),
463 }
464 }
465}
466
467impl VerificationResult<Transaction> {
468 pub fn advanced_commerce_info(
470 &self,
471 ) -> Result<Option<TransactionAdvancedCommerceInfo>, StoreKitError> {
472 parse_transaction_advanced_commerce_info_payload(&self.metadata().payload_data)
473 }
474}
475
476impl VerificationResult<RenewalInfo> {
477 pub fn advanced_commerce_info(
479 &self,
480 ) -> Result<Option<RenewalInfoAdvancedCommerceInfo>, StoreKitError> {
481 parse_renewal_advanced_commerce_info_payload(&self.metadata().payload_data)
482 }
483}
484
485#[derive(Debug, Deserialize)]
486pub(crate) struct TransactionAdvancedCommerceInfoPayload {
487 #[serde(rename = "requestReferenceID")]
488 request_reference_id: String,
489 #[serde(rename = "estimatedTax")]
490 estimated_tax: String,
491 #[serde(rename = "taxRate")]
492 tax_rate: String,
493 #[serde(rename = "taxCode")]
494 tax_code: String,
495 #[serde(rename = "taxExclusivePrice")]
496 tax_exclusive_price: String,
497 description: Option<String>,
498 #[serde(rename = "displayName")]
499 display_name: Option<String>,
500 period: Option<SubscriptionPeriodPayload>,
501 items: Vec<TransactionAdvancedCommerceItemPayload>,
502}
503
504impl TransactionAdvancedCommerceInfoPayload {
505 pub(crate) fn into_transaction_advanced_commerce_info(self) -> TransactionAdvancedCommerceInfo {
506 TransactionAdvancedCommerceInfo {
507 request_reference_id: self.request_reference_id,
508 estimated_tax: self.estimated_tax,
509 tax_rate: self.tax_rate,
510 tax_code: self.tax_code,
511 tax_exclusive_price: self.tax_exclusive_price,
512 description: self.description,
513 display_name: self.display_name,
514 period: self
515 .period
516 .map(SubscriptionPeriodPayload::into_subscription_period),
517 items: self
518 .items
519 .into_iter()
520 .map(
521 TransactionAdvancedCommerceItemPayload::into_transaction_advanced_commerce_item,
522 )
523 .collect(),
524 }
525 }
526}
527
528#[derive(Debug, Deserialize)]
529struct TransactionAdvancedCommerceItemPayload {
530 details: TransactionAdvancedCommerceItemDetailsPayload,
531 refunds: Option<Vec<TransactionAdvancedCommerceRefundPayload>>,
532 #[serde(rename = "revocationDate")]
533 revocation_date: Option<String>,
534}
535
536impl TransactionAdvancedCommerceItemPayload {
537 fn into_transaction_advanced_commerce_item(self) -> TransactionAdvancedCommerceItem {
538 TransactionAdvancedCommerceItem {
539 details: self.details.into_transaction_advanced_commerce_item_details(),
540 refunds: self.refunds.map(|refunds| {
541 refunds
542 .into_iter()
543 .map(TransactionAdvancedCommerceRefundPayload::into_transaction_advanced_commerce_refund)
544 .collect()
545 }),
546 revocation_date: self.revocation_date,
547 }
548 }
549}
550
551#[derive(Debug, Deserialize)]
552struct TransactionAdvancedCommerceItemDetailsPayload {
553 sku: String,
554 #[serde(rename = "displayName")]
555 display_name: String,
556 description: String,
557 offer: Option<TransactionAdvancedCommerceOfferPayload>,
558 price: String,
559}
560
561impl TransactionAdvancedCommerceItemDetailsPayload {
562 fn into_transaction_advanced_commerce_item_details(
563 self,
564 ) -> TransactionAdvancedCommerceItemDetails {
565 TransactionAdvancedCommerceItemDetails {
566 sku: self.sku,
567 display_name: self.display_name,
568 description: self.description,
569 offer: self.offer.map(
570 TransactionAdvancedCommerceOfferPayload::into_transaction_advanced_commerce_offer,
571 ),
572 price: self.price,
573 }
574 }
575}
576
577#[derive(Debug, Deserialize)]
578struct TransactionAdvancedCommerceOfferPayload {
579 price: String,
580 period: SubscriptionPeriodPayload,
581 #[serde(rename = "periodCount")]
582 period_count: i64,
583 reason: String,
584}
585
586impl TransactionAdvancedCommerceOfferPayload {
587 fn into_transaction_advanced_commerce_offer(self) -> TransactionAdvancedCommerceOffer {
588 TransactionAdvancedCommerceOffer {
589 price: self.price,
590 period: self.period.into_subscription_period(),
591 period_count: self.period_count,
592 reason: TransactionAdvancedCommerceOfferReason::from_raw(self.reason),
593 }
594 }
595}
596
597#[derive(Debug, Deserialize)]
598struct TransactionAdvancedCommerceRefundPayload {
599 reason: String,
600 #[serde(rename = "type")]
601 refund_type: String,
602 date: String,
603 amount: String,
604}
605
606impl TransactionAdvancedCommerceRefundPayload {
607 fn into_transaction_advanced_commerce_refund(self) -> TransactionAdvancedCommerceRefund {
608 TransactionAdvancedCommerceRefund {
609 reason: TransactionAdvancedCommerceRefundReason::from_raw(self.reason),
610 refund_type: TransactionAdvancedCommerceRefundType::from_raw(self.refund_type),
611 date: self.date,
612 amount: self.amount,
613 }
614 }
615}
616
617#[derive(Debug, Deserialize)]
618struct RenewalSignedPayload {
619 #[serde(rename = "advancedCommerceInfo")]
620 advanced_commerce_info: Option<RenewalInfoAdvancedCommerceInfoPayload>,
621}
622
623#[derive(Debug, Deserialize)]
624struct RenewalInfoAdvancedCommerceInfoPayload {
625 #[serde(rename = "consistencyToken")]
626 consistency_token: String,
627 #[serde(rename = "requestReferenceID")]
628 request_reference_id: String,
629 #[serde(rename = "taxCode")]
630 tax_code: String,
631 description: String,
632 #[serde(rename = "displayName")]
633 display_name: String,
634 period: SubscriptionPeriodPayload,
635 items: Vec<RenewalInfoAdvancedCommerceItemPayload>,
636}
637
638impl RenewalInfoAdvancedCommerceInfoPayload {
639 fn into_renewal_info_advanced_commerce_info(self) -> RenewalInfoAdvancedCommerceInfo {
640 RenewalInfoAdvancedCommerceInfo {
641 consistency_token: self.consistency_token,
642 request_reference_id: self.request_reference_id,
643 tax_code: self.tax_code,
644 description: self.description,
645 display_name: self.display_name,
646 period: self.period.into_subscription_period(),
647 items: self
648 .items
649 .into_iter()
650 .map(RenewalInfoAdvancedCommerceItemPayload::into_renewal_info_advanced_commerce_item)
651 .collect(),
652 }
653 }
654}
655
656#[derive(Debug, Deserialize)]
657struct RenewalInfoAdvancedCommerceItemPayload {
658 details: TransactionAdvancedCommerceItemDetailsPayload,
659 #[serde(rename = "priceIncreaseInfo")]
660 price_increase_info: Option<RenewalInfoAdvancedCommercePriceIncreaseInfoPayload>,
661}
662
663impl RenewalInfoAdvancedCommerceItemPayload {
664 fn into_renewal_info_advanced_commerce_item(self) -> RenewalInfoAdvancedCommerceItem {
665 RenewalInfoAdvancedCommerceItem {
666 details: self.details.into_transaction_advanced_commerce_item_details(),
667 price_increase_info: self.price_increase_info.map(
668 RenewalInfoAdvancedCommercePriceIncreaseInfoPayload::into_renewal_info_advanced_commerce_price_increase_info,
669 ),
670 }
671 }
672}
673
674#[derive(Debug, Deserialize)]
675struct RenewalInfoAdvancedCommercePriceIncreaseInfoPayload {
676 status: String,
677 price: String,
678 #[serde(rename = "dependentSKUs")]
679 dependent_skus: Vec<String>,
680}
681
682impl RenewalInfoAdvancedCommercePriceIncreaseInfoPayload {
683 fn into_renewal_info_advanced_commerce_price_increase_info(
684 self,
685 ) -> RenewalInfoAdvancedCommercePriceIncreaseInfo {
686 RenewalInfoAdvancedCommercePriceIncreaseInfo {
687 status: RenewalInfoAdvancedCommercePriceIncreaseStatus::from_raw(self.status),
688 price: self.price,
689 dependent_skus: self.dependent_skus,
690 }
691 }
692}
693
694#[derive(Debug, Deserialize)]
695struct AdvancedCommerceProductPayload {
696 id: String,
697 #[serde(rename = "type")]
698 product_type: String,
699}
700
701impl AdvancedCommerceProductPayload {
702 fn into_product(self) -> AdvancedCommerceProduct {
703 AdvancedCommerceProduct {
704 id: self.id,
705 product_type: ProductType::from_raw(self.product_type),
706 }
707 }
708}
709
710#[derive(Debug, Deserialize)]
711#[allow(clippy::unsafe_derive_deserialize)]
712pub(crate) struct AppStoreMerchandisingPresentationResultPayload {
713 kind: String,
714 #[serde(rename = "purchaseResult")]
715 purchase_result: Option<PurchaseResultPayload>,
716}
717
718impl AppStoreMerchandisingPresentationResultPayload {
719 pub(crate) fn into_result(
720 self,
721 transaction_handle: *mut c_void,
722 ) -> Result<AppStoreMerchandisingPresentationResult, StoreKitError> {
723 match self.kind.as_str() {
724 "dismissed" => {
725 if !transaction_handle.is_null() {
726 unsafe { ffi::sk_transaction_release(transaction_handle) };
727 }
728 Ok(AppStoreMerchandisingPresentationResult::Dismissed)
729 }
730 "purchaseCompleted" => {
731 let purchase_result = self.purchase_result.ok_or_else(|| {
732 StoreKitError::Unknown(
733 "App Store merchandising reported a purchase completion without a purchase result"
734 .to_owned(),
735 )
736 })?;
737 Ok(AppStoreMerchandisingPresentationResult::PurchaseCompleted(
738 purchase_result.into_purchase_result(transaction_handle)?,
739 ))
740 }
741 other => {
742 if !transaction_handle.is_null() {
743 unsafe { ffi::sk_transaction_release(transaction_handle) };
744 }
745 Err(StoreKitError::Unknown(format!(
746 "unknown App Store merchandising presentation result kind '{other}'"
747 )))
748 }
749 }
750 }
751}
752
753#[derive(Debug, Deserialize)]
754struct TransactionSignedPayload {
755 #[serde(rename = "advancedCommerceInfo")]
756 advanced_commerce_info: Option<TransactionAdvancedCommerceInfoPayload>,
757}
758
759fn parse_transaction_advanced_commerce_info_payload(
760 payload_data: &[u8],
761) -> Result<Option<TransactionAdvancedCommerceInfo>, StoreKitError> {
762 let payload =
763 serde_json::from_slice::<TransactionSignedPayload>(payload_data).map_err(|error| {
764 StoreKitError::InvalidArgument(format!(
765 "failed to parse signed transaction payload JSON: {error}"
766 ))
767 })?;
768 Ok(payload
769 .advanced_commerce_info
770 .map(TransactionAdvancedCommerceInfoPayload::into_transaction_advanced_commerce_info))
771}
772
773fn parse_renewal_advanced_commerce_info_payload(
774 payload_data: &[u8],
775) -> Result<Option<RenewalInfoAdvancedCommerceInfo>, StoreKitError> {
776 let payload =
777 serde_json::from_slice::<RenewalSignedPayload>(payload_data).map_err(|error| {
778 StoreKitError::InvalidArgument(format!(
779 "failed to parse signed renewal payload JSON: {error}"
780 ))
781 })?;
782 Ok(payload
783 .advanced_commerce_info
784 .map(RenewalInfoAdvancedCommerceInfoPayload::into_renewal_info_advanced_commerce_info))
785}