1use core::ffi::c_void;
2use core::ptr;
3use std::ptr::NonNull;
4use std::time::Duration;
5
6use serde::{Deserialize, Serialize};
7
8use crate::advanced_commerce::{
9 TransactionAdvancedCommerceInfo, TransactionAdvancedCommerceInfoPayload,
10};
11use crate::app_store::AppStoreEnvironment;
12use crate::error::{StoreKitError, VerificationFailure};
13use crate::ffi;
14use crate::private::{
15 cstring_from_str, decode_base64, duration_to_timeout_ms, error_from_status, json_cstring,
16 parse_json_ptr, parse_optional_json_ptr,
17};
18use crate::product::ProductType;
19use crate::refund::{Refund, RefundRequestStatus};
20use crate::storefront::{Storefront, StorefrontPayload};
21use crate::subscription::{SubscriptionPeriod, SubscriptionPeriodPayload};
22use crate::subscription_info::BillingPlanType;
23pub use crate::verification_result::VerificationResult;
24
25use crate::verification_result::VerificationResultPayload;
26
27#[derive(Debug, Clone, PartialEq, Eq)]
28pub enum TransactionReason {
30 Purchase,
32 Renewal,
34 Unknown(String),
36}
37
38impl TransactionReason {
39 pub fn as_str(&self) -> &str {
41 match self {
42 Self::Purchase => "purchase",
43 Self::Renewal => "renewal",
44 Self::Unknown(value) => value.as_str(),
45 }
46 }
47
48 fn from_raw(raw: String) -> Self {
49 match raw.as_str() {
50 "purchase" => Self::Purchase,
51 "renewal" => Self::Renewal,
52 _ => Self::Unknown(raw),
53 }
54 }
55}
56
57#[derive(Debug, Clone, PartialEq, Eq)]
58pub enum RevocationReason {
60 DeveloperIssue,
62 Other,
64 Unknown(String),
66}
67
68impl RevocationReason {
69 pub fn as_str(&self) -> &str {
71 match self {
72 Self::DeveloperIssue => "developerIssue",
73 Self::Other => "other",
74 Self::Unknown(value) => value.as_str(),
75 }
76 }
77
78 fn from_raw(raw: String) -> Self {
79 match raw.as_str() {
80 "developerIssue" => Self::DeveloperIssue,
81 "other" => Self::Other,
82 _ => Self::Unknown(raw),
83 }
84 }
85}
86
87#[derive(Debug, Clone, PartialEq, Eq)]
88pub enum RevocationType {
90 FamilyRevocation,
92 FullRefund,
94 ProratedRefund,
96 Unknown(String),
98}
99
100impl RevocationType {
101 pub fn as_str(&self) -> &str {
103 match self {
104 Self::FamilyRevocation => "familyRevocation",
105 Self::FullRefund => "fullRefund",
106 Self::ProratedRefund => "proratedRefund",
107 Self::Unknown(value) => value.as_str(),
108 }
109 }
110
111 fn from_raw(raw: String) -> Self {
112 match raw.as_str() {
113 "familyRevocation" => Self::FamilyRevocation,
114 "fullRefund" => Self::FullRefund,
115 "proratedRefund" => Self::ProratedRefund,
116 _ => Self::Unknown(raw),
117 }
118 }
119}
120
121#[derive(Debug, Clone, PartialEq, Eq)]
122pub enum OfferType {
124 Introductory,
126 Promotional,
128 Code,
130 WinBack,
132 Unknown(String),
134}
135
136impl OfferType {
137 pub fn as_str(&self) -> &str {
139 match self {
140 Self::Introductory => "introductory",
141 Self::Promotional => "promotional",
142 Self::Code => "code",
143 Self::WinBack => "winBack",
144 Self::Unknown(value) => value.as_str(),
145 }
146 }
147
148 fn from_raw(raw: String) -> Self {
149 match raw.as_str() {
150 "introductory" => Self::Introductory,
151 "promotional" => Self::Promotional,
152 "code" => Self::Code,
153 "winBack" => Self::WinBack,
154 _ => Self::Unknown(raw),
155 }
156 }
157}
158
159#[derive(Debug, Clone, PartialEq, Eq)]
160pub enum OfferPaymentMode {
162 FreeTrial,
164 PayAsYouGo,
166 PayUpFront,
168 OneTime,
170 Unknown(String),
172}
173
174impl OfferPaymentMode {
175 pub fn as_str(&self) -> &str {
177 match self {
178 Self::FreeTrial => "freeTrial",
179 Self::PayAsYouGo => "payAsYouGo",
180 Self::PayUpFront => "payUpFront",
181 Self::OneTime => "oneTime",
182 Self::Unknown(value) => value.as_str(),
183 }
184 }
185
186 fn from_raw(raw: String) -> Self {
187 match raw.as_str() {
188 "freeTrial" => Self::FreeTrial,
189 "payAsYouGo" => Self::PayAsYouGo,
190 "payUpFront" => Self::PayUpFront,
191 "oneTime" => Self::OneTime,
192 _ => Self::Unknown(raw),
193 }
194 }
195}
196
197#[derive(Debug, Clone, PartialEq, Eq)]
198pub struct TransactionOffer {
200 pub id: Option<String>,
202 pub offer_type: OfferType,
204 pub payment_mode: Option<OfferPaymentMode>,
206 pub period: Option<SubscriptionPeriod>,
208}
209
210#[derive(Debug, Clone, PartialEq, Eq)]
211pub struct TransactionCommitmentInfo {
213 pub billing_period_number: u64,
215 pub total_billing_periods: u64,
217 pub expiration_date: String,
219 pub price: String,
221}
222
223#[derive(Debug, Clone, PartialEq, Eq)]
224pub enum OwnershipType {
226 Purchased,
228 FamilyShared,
230 Unknown(String),
232}
233
234impl OwnershipType {
235 fn from_raw(raw: String) -> Self {
236 match raw.as_str() {
237 "purchased" => Self::Purchased,
238 "familyShared" => Self::FamilyShared,
239 _ => Self::Unknown(raw),
240 }
241 }
242}
243
244#[derive(Debug, Clone, PartialEq, Eq)]
245pub struct TransactionData {
247 pub id: u64,
249 pub original_id: u64,
251 pub web_order_line_item_id: Option<String>,
253 pub product_id: String,
255 pub subscription_group_id: Option<String>,
257 pub app_bundle_id: String,
259 pub purchase_date: String,
261 pub original_purchase_date: String,
263 pub expiration_date: Option<String>,
265 pub purchased_quantity: u64,
267 pub is_upgraded: bool,
269 pub ownership_type: OwnershipType,
271 pub signed_date: String,
273 pub jws_representation: String,
275 pub verification_failure: Option<VerificationFailure>,
277 pub revocation_date: Option<String>,
279 pub revocation_reason: Option<RevocationReason>,
281 pub revocation_type: Option<RevocationType>,
283 pub product_type: Option<ProductType>,
285 pub app_account_token: Option<String>,
287 pub environment: Option<AppStoreEnvironment>,
289 pub reason: Option<TransactionReason>,
291 pub storefront: Option<Storefront>,
293 pub price: Option<String>,
295 pub currency_code: Option<String>,
297 pub billing_plan_type: Option<BillingPlanType>,
299 pub commitment_info: Option<TransactionCommitmentInfo>,
301 pub app_transaction_id: Option<String>,
303 pub offer: Option<TransactionOffer>,
305 pub json_representation: Vec<u8>,
307}
308
309#[derive(Debug)]
310pub struct Transaction {
312 handle: Option<NonNull<c_void>>,
313 data: TransactionData,
314 advanced_commerce_info: Option<TransactionAdvancedCommerceInfo>,
315}
316
317impl Clone for Transaction {
318 fn clone(&self) -> Self {
319 let handle = self.handle.map(|handle| {
320 let retained = unsafe { ffi::sk_transaction_retain(handle.as_ptr()) };
324 NonNull::new(retained).expect("StoreKit transaction retain returned null")
325 });
326 Self {
327 handle,
328 data: self.data.clone(),
329 advanced_commerce_info: self.advanced_commerce_info.clone(),
330 }
331 }
332}
333
334impl Drop for Transaction {
335 fn drop(&mut self) {
336 if let Some(handle) = self.handle {
337 unsafe { ffi::sk_transaction_release(handle.as_ptr()) };
341 }
342 }
343}
344
345impl Transaction {
346 pub fn current_entitlements() -> Result<TransactionStream, StoreKitError> {
348 TransactionStream::new(&TransactionStreamConfig::current_entitlements())
349 }
350
351 pub fn all() -> Result<TransactionStream, StoreKitError> {
353 TransactionStream::new(&TransactionStreamConfig::all())
354 }
355
356 pub fn updates() -> Result<TransactionStream, StoreKitError> {
358 TransactionStream::new(&TransactionStreamConfig::updates())
359 }
360
361 pub fn unfinished() -> Result<TransactionStream, StoreKitError> {
363 TransactionStream::new(&TransactionStreamConfig::unfinished())
364 }
365
366 pub fn all_for(product_id: &str) -> Result<TransactionStream, StoreKitError> {
368 TransactionStream::new(&TransactionStreamConfig::all_for(product_id))
369 }
370
371 pub fn current_entitlements_for(product_id: &str) -> Result<TransactionStream, StoreKitError> {
373 TransactionStream::new(&TransactionStreamConfig::current_entitlements_for(
374 product_id,
375 ))
376 }
377
378 pub fn latest_for(product_id: &str) -> Result<Option<VerificationResult<Self>>, StoreKitError> {
380 let product_id = cstring_from_str(product_id, "product id")?;
381 let mut transaction_handle = ptr::null_mut();
382 let mut result_json = ptr::null_mut();
383 let mut error_message = ptr::null_mut();
384 let status = unsafe {
385 ffi::sk_transaction_latest_for(
386 product_id.as_ptr(),
387 &mut transaction_handle,
388 &mut result_json,
389 &mut error_message,
390 )
391 };
392 if status != ffi::status::OK {
393 return Err(unsafe { error_from_status(status, error_message) });
394 }
395
396 let payload = unsafe {
397 parse_optional_json_ptr::<VerificationResultPayload<TransactionPayload>>(
398 result_json,
399 "latest transaction",
400 )
401 }?;
402 payload
403 .map(|payload| {
404 payload.into_result(|payload| Self::from_raw_parts(transaction_handle, payload))
405 })
406 .transpose()
407 }
408
409 pub fn current_entitlement_for(
411 product_id: &str,
412 ) -> Result<Option<VerificationResult<Self>>, StoreKitError> {
413 let product_id = cstring_from_str(product_id, "product id")?;
414 let mut transaction_handle = ptr::null_mut();
415 let mut result_json = ptr::null_mut();
416 let mut error_message = ptr::null_mut();
417 let status = unsafe {
418 ffi::sk_transaction_current_entitlement_for(
419 product_id.as_ptr(),
420 &mut transaction_handle,
421 &mut result_json,
422 &mut error_message,
423 )
424 };
425 if status != ffi::status::OK {
426 return Err(unsafe { error_from_status(status, error_message) });
427 }
428
429 let payload = unsafe {
430 parse_optional_json_ptr::<VerificationResultPayload<TransactionPayload>>(
431 result_json,
432 "current entitlement transaction",
433 )
434 }?;
435 payload
436 .map(|payload| {
437 payload.into_result(|payload| Self::from_raw_parts(transaction_handle, payload))
438 })
439 .transpose()
440 }
441
442 pub const fn data(&self) -> &TransactionData {
444 &self.data
445 }
446
447 pub const fn advanced_commerce_info(&self) -> Option<&TransactionAdvancedCommerceInfo> {
449 self.advanced_commerce_info.as_ref()
450 }
451
452 pub const fn has_live_handle(&self) -> bool {
454 self.handle.is_some()
455 }
456
457 pub fn verify(&self) -> Result<(), StoreKitError> {
459 self.handle.map_or_else(
460 || {
461 self.data
462 .verification_failure
463 .clone()
464 .map_or(Ok(()), |failure| Err(StoreKitError::Verification(failure)))
465 },
466 |handle| {
467 let mut error_message = ptr::null_mut();
468 let status =
469 unsafe { ffi::sk_transaction_verify(handle.as_ptr(), &mut error_message) };
470 if status == ffi::status::OK {
471 Ok(())
472 } else {
473 Err(unsafe { error_from_status(status, error_message) })
474 }
475 },
476 )
477 }
478
479 pub fn finish(&self) -> Result<(), StoreKitError> {
481 self.handle.map_or_else(
482 || {
483 Err(StoreKitError::NotSupported(
484 "transaction snapshots cannot be finished because they do not carry a live StoreKit handle"
485 .to_owned(),
486 ))
487 },
488 |handle| {
489 let mut error_message = ptr::null_mut();
490 let status = unsafe { ffi::sk_transaction_finish(handle.as_ptr(), &mut error_message) };
491 if status == ffi::status::OK {
492 Ok(())
493 } else {
494 Err(unsafe { error_from_status(status, error_message) })
495 }
496 },
497 )
498 }
499
500 pub fn begin_refund_request(&self) -> Result<RefundRequestStatus, StoreKitError> {
502 Refund::begin_for_transaction_id(self.data.id)
503 }
504
505 pub(crate) fn from_raw_parts(
506 handle: *mut c_void,
507 payload: TransactionPayload,
508 ) -> Result<Self, StoreKitError> {
509 let (data, advanced_commerce_info) = payload.into_transaction_parts()?;
510 Ok(Self {
511 handle: NonNull::new(handle),
512 data,
513 advanced_commerce_info,
514 })
515 }
516
517 pub(crate) fn from_snapshot_payload(
518 payload: TransactionPayload,
519 ) -> Result<Self, StoreKitError> {
520 let (data, advanced_commerce_info) = payload.into_transaction_parts()?;
521 Ok(Self {
522 handle: None,
523 data,
524 advanced_commerce_info,
525 })
526 }
527}
528
529#[derive(Debug)]
530pub struct TransactionStream {
532 handle: NonNull<c_void>,
533 finished: bool,
534}
535
536impl Drop for TransactionStream {
537 fn drop(&mut self) {
538 unsafe { ffi::sk_transaction_stream_release(self.handle.as_ptr()) };
539 }
540}
541
542impl TransactionStream {
543 fn new(config: &TransactionStreamConfig) -> Result<Self, StoreKitError> {
544 let config_json = json_cstring(config, "transaction stream config")?;
545 let mut error_message = ptr::null_mut();
546 let handle =
547 unsafe { ffi::sk_transaction_stream_create(config_json.as_ptr(), &mut error_message) };
548 let handle = NonNull::new(handle)
549 .ok_or_else(|| unsafe { error_from_status(ffi::status::UNKNOWN, error_message) })?;
550 Ok(Self {
551 handle,
552 finished: false,
553 })
554 }
555
556 pub const fn is_finished(&self) -> bool {
558 self.finished
559 }
560
561 #[allow(clippy::should_implement_trait)]
562 pub fn next(&mut self) -> Result<Option<VerificationResult<Transaction>>, StoreKitError> {
564 self.next_timeout(Duration::from_secs(30))
565 }
566
567 pub fn next_timeout(
569 &mut self,
570 timeout: Duration,
571 ) -> Result<Option<VerificationResult<Transaction>>, StoreKitError> {
572 let mut transaction_handle = ptr::null_mut();
573 let mut verification_json = ptr::null_mut();
574 let mut error_message = ptr::null_mut();
575 let status = unsafe {
576 ffi::sk_transaction_stream_next(
577 self.handle.as_ptr(),
578 duration_to_timeout_ms(timeout),
579 &mut transaction_handle,
580 &mut verification_json,
581 &mut error_message,
582 )
583 };
584
585 match status {
586 ffi::status::OK => {
587 let payload = unsafe {
588 parse_json_ptr::<VerificationResultPayload<TransactionPayload>>(
589 verification_json,
590 "transaction verification result",
591 )
592 };
593 match payload {
594 Ok(payload) => payload
595 .into_result(|payload| {
596 Transaction::from_raw_parts(transaction_handle, payload)
597 })
598 .map(Some),
599 Err(error) => {
600 if !transaction_handle.is_null() {
601 unsafe { ffi::sk_transaction_release(transaction_handle) };
602 }
603 Err(error)
604 }
605 }
606 }
607 ffi::status::END_OF_STREAM => {
608 self.finished = true;
609 Ok(None)
610 }
611 ffi::status::TIMED_OUT => Ok(None),
612 _ => Err(unsafe { error_from_status(status, error_message) }),
613 }
614 }
615}
616
617#[derive(Debug, Serialize)]
618struct TransactionStreamConfig {
619 kind: &'static str,
620 #[serde(rename = "productID", skip_serializing_if = "Option::is_none")]
621 product_id: Option<String>,
622}
623
624impl TransactionStreamConfig {
625 const fn all() -> Self {
626 Self {
627 kind: "all",
628 product_id: None,
629 }
630 }
631
632 const fn current_entitlements() -> Self {
633 Self {
634 kind: "currentEntitlements",
635 product_id: None,
636 }
637 }
638
639 const fn updates() -> Self {
640 Self {
641 kind: "updates",
642 product_id: None,
643 }
644 }
645
646 const fn unfinished() -> Self {
647 Self {
648 kind: "unfinished",
649 product_id: None,
650 }
651 }
652
653 fn all_for(product_id: &str) -> Self {
654 Self {
655 kind: "allFor",
656 product_id: Some(product_id.to_owned()),
657 }
658 }
659
660 fn current_entitlements_for(product_id: &str) -> Self {
661 Self {
662 kind: "currentEntitlementsFor",
663 product_id: Some(product_id.to_owned()),
664 }
665 }
666}
667
668#[derive(Debug, Deserialize)]
669pub(crate) struct TransactionOfferPayload {
670 id: Option<String>,
671 #[serde(rename = "type")]
672 offer_type: String,
673 #[serde(rename = "paymentMode")]
674 payment_mode: Option<String>,
675 period: Option<SubscriptionPeriodPayload>,
676}
677
678impl TransactionOfferPayload {
679 pub(crate) fn into_transaction_offer(self) -> TransactionOffer {
680 TransactionOffer {
681 id: self.id,
682 offer_type: OfferType::from_raw(self.offer_type),
683 payment_mode: self.payment_mode.map(OfferPaymentMode::from_raw),
684 period: self
685 .period
686 .map(SubscriptionPeriodPayload::into_subscription_period),
687 }
688 }
689}
690
691#[derive(Debug, Deserialize)]
692pub(crate) struct TransactionCommitmentInfoPayload {
693 #[serde(rename = "billingPeriodNumber")]
694 billing_period_number: u64,
695 #[serde(rename = "totalBillingPeriods")]
696 total_billing_periods: u64,
697 #[serde(rename = "expirationDate")]
698 expiration_date: String,
699 price: String,
700}
701
702impl TransactionCommitmentInfoPayload {
703 pub(crate) fn into_transaction_commitment_info(self) -> TransactionCommitmentInfo {
704 TransactionCommitmentInfo {
705 billing_period_number: self.billing_period_number,
706 total_billing_periods: self.total_billing_periods,
707 expiration_date: self.expiration_date,
708 price: self.price,
709 }
710 }
711}
712
713#[derive(Debug, Deserialize)]
714pub(crate) struct TransactionPayload {
715 id: u64,
716 #[serde(rename = "originalID")]
717 original_id: u64,
718 #[serde(rename = "webOrderLineItemID")]
719 web_order_line_item_id: Option<String>,
720 #[serde(rename = "productID")]
721 product_id: String,
722 #[serde(rename = "subscriptionGroupID")]
723 subscription_group_id: Option<String>,
724 #[serde(rename = "appBundleID")]
725 app_bundle_id: String,
726 #[serde(rename = "purchaseDate")]
727 purchase_date: String,
728 #[serde(rename = "originalPurchaseDate")]
729 original_purchase_date: String,
730 #[serde(rename = "expirationDate")]
731 expiration_date: Option<String>,
732 #[serde(rename = "purchasedQuantity")]
733 purchased_quantity: u64,
734 #[serde(rename = "isUpgraded")]
735 is_upgraded: bool,
736 #[serde(rename = "ownershipType")]
737 ownership_type: String,
738 #[serde(rename = "signedDate")]
739 signed_date: String,
740 #[serde(rename = "jwsRepresentation")]
741 jws_representation: String,
742 #[serde(rename = "verificationError")]
743 verification_error: Option<crate::error::VerificationErrorPayload>,
744 #[serde(rename = "revocationDate")]
745 revocation_date: Option<String>,
746 #[serde(rename = "revocationReason")]
747 revocation_reason: Option<String>,
748 #[serde(rename = "revocationType")]
749 revocation_type: Option<String>,
750 #[serde(rename = "productType")]
751 product_type: Option<String>,
752 #[serde(rename = "appAccountToken")]
753 app_account_token: Option<String>,
754 environment: Option<String>,
755 reason: Option<String>,
756 storefront: Option<StorefrontPayload>,
757 price: Option<String>,
758 #[serde(rename = "currencyCode")]
759 currency_code: Option<String>,
760 #[serde(rename = "billingPlanType")]
761 billing_plan_type: Option<String>,
762 #[serde(rename = "commitmentInfo")]
763 commitment_info: Option<TransactionCommitmentInfoPayload>,
764 #[serde(rename = "appTransactionID")]
765 app_transaction_id: Option<String>,
766 offer: Option<TransactionOfferPayload>,
767 #[serde(rename = "advancedCommerceInfo")]
768 advanced_commerce_info: Option<TransactionAdvancedCommerceInfoPayload>,
769 #[serde(rename = "jsonRepresentationBase64")]
770 json_representation_base64: String,
771}
772
773impl TransactionPayload {
774 fn into_transaction_parts(
775 self,
776 ) -> Result<(TransactionData, Option<TransactionAdvancedCommerceInfo>), StoreKitError> {
777 let advanced_commerce_info = self
778 .advanced_commerce_info
779 .map(TransactionAdvancedCommerceInfoPayload::into_transaction_advanced_commerce_info);
780 Ok((
781 TransactionData {
782 id: self.id,
783 original_id: self.original_id,
784 web_order_line_item_id: self.web_order_line_item_id,
785 product_id: self.product_id,
786 subscription_group_id: self.subscription_group_id,
787 app_bundle_id: self.app_bundle_id,
788 purchase_date: self.purchase_date,
789 original_purchase_date: self.original_purchase_date,
790 expiration_date: self.expiration_date,
791 purchased_quantity: self.purchased_quantity,
792 is_upgraded: self.is_upgraded,
793 ownership_type: OwnershipType::from_raw(self.ownership_type),
794 signed_date: self.signed_date,
795 jws_representation: self.jws_representation,
796 verification_failure: self
797 .verification_error
798 .map(crate::error::VerificationFailure::from_payload),
799 revocation_date: self.revocation_date,
800 revocation_reason: self.revocation_reason.map(RevocationReason::from_raw),
801 revocation_type: self.revocation_type.map(RevocationType::from_raw),
802 product_type: self.product_type.map(ProductType::from_raw),
803 app_account_token: self.app_account_token,
804 environment: self.environment.map(AppStoreEnvironment::from_raw),
805 reason: self.reason.map(TransactionReason::from_raw),
806 storefront: self.storefront.map(StorefrontPayload::into_storefront),
807 price: self.price,
808 currency_code: self.currency_code,
809 billing_plan_type: self.billing_plan_type.map(BillingPlanType::from_raw),
810 commitment_info: self
811 .commitment_info
812 .map(TransactionCommitmentInfoPayload::into_transaction_commitment_info),
813 app_transaction_id: self.app_transaction_id,
814 offer: self
815 .offer
816 .map(TransactionOfferPayload::into_transaction_offer),
817 json_representation: decode_base64(
818 &self.json_representation_base64,
819 "transaction JSON representation",
820 )?,
821 },
822 advanced_commerce_info,
823 ))
824 }
825}