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};
22pub use crate::verification_result::VerificationResult;
23
24use crate::verification_result::VerificationResultPayload;
25
26#[derive(Debug, Clone, PartialEq, Eq)]
27pub enum TransactionReason {
29 Purchase,
31 Renewal,
33 Unknown(String),
35}
36
37impl TransactionReason {
38 pub fn as_str(&self) -> &str {
40 match self {
41 Self::Purchase => "purchase",
42 Self::Renewal => "renewal",
43 Self::Unknown(value) => value.as_str(),
44 }
45 }
46
47 fn from_raw(raw: String) -> Self {
48 match raw.as_str() {
49 "purchase" => Self::Purchase,
50 "renewal" => Self::Renewal,
51 _ => Self::Unknown(raw),
52 }
53 }
54}
55
56#[derive(Debug, Clone, PartialEq, Eq)]
57pub enum RevocationReason {
59 DeveloperIssue,
61 Other,
63 Unknown(String),
65}
66
67impl RevocationReason {
68 pub fn as_str(&self) -> &str {
70 match self {
71 Self::DeveloperIssue => "developerIssue",
72 Self::Other => "other",
73 Self::Unknown(value) => value.as_str(),
74 }
75 }
76
77 fn from_raw(raw: String) -> Self {
78 match raw.as_str() {
79 "developerIssue" => Self::DeveloperIssue,
80 "other" => Self::Other,
81 _ => Self::Unknown(raw),
82 }
83 }
84}
85
86#[derive(Debug, Clone, PartialEq, Eq)]
87pub enum OfferType {
89 Introductory,
91 Promotional,
93 Code,
95 WinBack,
97 Unknown(String),
99}
100
101impl OfferType {
102 pub fn as_str(&self) -> &str {
104 match self {
105 Self::Introductory => "introductory",
106 Self::Promotional => "promotional",
107 Self::Code => "code",
108 Self::WinBack => "winBack",
109 Self::Unknown(value) => value.as_str(),
110 }
111 }
112
113 fn from_raw(raw: String) -> Self {
114 match raw.as_str() {
115 "introductory" => Self::Introductory,
116 "promotional" => Self::Promotional,
117 "code" => Self::Code,
118 "winBack" => Self::WinBack,
119 _ => Self::Unknown(raw),
120 }
121 }
122}
123
124#[derive(Debug, Clone, PartialEq, Eq)]
125pub enum OfferPaymentMode {
127 FreeTrial,
129 PayAsYouGo,
131 PayUpFront,
133 OneTime,
135 Unknown(String),
137}
138
139impl OfferPaymentMode {
140 pub fn as_str(&self) -> &str {
142 match self {
143 Self::FreeTrial => "freeTrial",
144 Self::PayAsYouGo => "payAsYouGo",
145 Self::PayUpFront => "payUpFront",
146 Self::OneTime => "oneTime",
147 Self::Unknown(value) => value.as_str(),
148 }
149 }
150
151 fn from_raw(raw: String) -> Self {
152 match raw.as_str() {
153 "freeTrial" => Self::FreeTrial,
154 "payAsYouGo" => Self::PayAsYouGo,
155 "payUpFront" => Self::PayUpFront,
156 "oneTime" => Self::OneTime,
157 _ => Self::Unknown(raw),
158 }
159 }
160}
161
162#[derive(Debug, Clone, PartialEq, Eq)]
163pub struct TransactionOffer {
165 pub id: Option<String>,
167 pub offer_type: OfferType,
169 pub payment_mode: Option<OfferPaymentMode>,
171 pub period: Option<SubscriptionPeriod>,
173}
174
175#[derive(Debug, Clone, PartialEq, Eq)]
176pub enum OwnershipType {
178 Purchased,
180 FamilyShared,
182 Unknown(String),
184}
185
186impl OwnershipType {
187 fn from_raw(raw: String) -> Self {
188 match raw.as_str() {
189 "purchased" => Self::Purchased,
190 "familyShared" => Self::FamilyShared,
191 _ => Self::Unknown(raw),
192 }
193 }
194}
195
196#[derive(Debug, Clone, PartialEq, Eq)]
197pub struct TransactionData {
199 pub id: u64,
201 pub original_id: u64,
203 pub web_order_line_item_id: Option<String>,
205 pub product_id: String,
207 pub subscription_group_id: Option<String>,
209 pub app_bundle_id: String,
211 pub purchase_date: String,
213 pub original_purchase_date: String,
215 pub expiration_date: Option<String>,
217 pub purchased_quantity: u64,
219 pub is_upgraded: bool,
221 pub ownership_type: OwnershipType,
223 pub signed_date: String,
225 pub jws_representation: String,
227 pub verification_failure: Option<VerificationFailure>,
229 pub revocation_date: Option<String>,
231 pub revocation_reason: Option<RevocationReason>,
233 pub product_type: Option<ProductType>,
235 pub app_account_token: Option<String>,
237 pub environment: Option<AppStoreEnvironment>,
239 pub reason: Option<TransactionReason>,
241 pub storefront: Option<Storefront>,
243 pub price: Option<String>,
245 pub currency_code: Option<String>,
247 pub app_transaction_id: Option<String>,
249 pub offer: Option<TransactionOffer>,
251 pub json_representation: Vec<u8>,
253}
254
255#[derive(Debug)]
256pub struct Transaction {
258 handle: Option<NonNull<c_void>>,
259 data: TransactionData,
260 advanced_commerce_info: Option<TransactionAdvancedCommerceInfo>,
261}
262
263impl Clone for Transaction {
264 fn clone(&self) -> Self {
265 let handle = self.handle.map(|handle| {
266 let retained = unsafe { ffi::sk_transaction_retain(handle.as_ptr()) };
270 NonNull::new(retained).expect("StoreKit transaction retain returned null")
271 });
272 Self {
273 handle,
274 data: self.data.clone(),
275 advanced_commerce_info: self.advanced_commerce_info.clone(),
276 }
277 }
278}
279
280impl Drop for Transaction {
281 fn drop(&mut self) {
282 if let Some(handle) = self.handle {
283 unsafe { ffi::sk_transaction_release(handle.as_ptr()) };
287 }
288 }
289}
290
291impl Transaction {
292 pub fn current_entitlements() -> Result<TransactionStream, StoreKitError> {
294 TransactionStream::new(&TransactionStreamConfig::current_entitlements())
295 }
296
297 pub fn all() -> Result<TransactionStream, StoreKitError> {
299 TransactionStream::new(&TransactionStreamConfig::all())
300 }
301
302 pub fn updates() -> Result<TransactionStream, StoreKitError> {
304 TransactionStream::new(&TransactionStreamConfig::updates())
305 }
306
307 pub fn unfinished() -> Result<TransactionStream, StoreKitError> {
309 TransactionStream::new(&TransactionStreamConfig::unfinished())
310 }
311
312 pub fn all_for(product_id: &str) -> Result<TransactionStream, StoreKitError> {
314 TransactionStream::new(&TransactionStreamConfig::all_for(product_id))
315 }
316
317 pub fn current_entitlements_for(product_id: &str) -> Result<TransactionStream, StoreKitError> {
319 TransactionStream::new(&TransactionStreamConfig::current_entitlements_for(
320 product_id,
321 ))
322 }
323
324 pub fn latest_for(product_id: &str) -> Result<Option<VerificationResult<Self>>, StoreKitError> {
326 let product_id = cstring_from_str(product_id, "product id")?;
327 let mut transaction_handle = ptr::null_mut();
328 let mut result_json = ptr::null_mut();
329 let mut error_message = ptr::null_mut();
330 let status = unsafe {
331 ffi::sk_transaction_latest_for(
332 product_id.as_ptr(),
333 &mut transaction_handle,
334 &mut result_json,
335 &mut error_message,
336 )
337 };
338 if status != ffi::status::OK {
339 return Err(unsafe { error_from_status(status, error_message) });
340 }
341
342 let payload = unsafe {
343 parse_optional_json_ptr::<VerificationResultPayload<TransactionPayload>>(
344 result_json,
345 "latest transaction",
346 )
347 }?;
348 payload
349 .map(|payload| {
350 payload.into_result(|payload| Self::from_raw_parts(transaction_handle, payload))
351 })
352 .transpose()
353 }
354
355 pub fn current_entitlement_for(
357 product_id: &str,
358 ) -> Result<Option<VerificationResult<Self>>, StoreKitError> {
359 let product_id = cstring_from_str(product_id, "product id")?;
360 let mut transaction_handle = ptr::null_mut();
361 let mut result_json = ptr::null_mut();
362 let mut error_message = ptr::null_mut();
363 let status = unsafe {
364 ffi::sk_transaction_current_entitlement_for(
365 product_id.as_ptr(),
366 &mut transaction_handle,
367 &mut result_json,
368 &mut error_message,
369 )
370 };
371 if status != ffi::status::OK {
372 return Err(unsafe { error_from_status(status, error_message) });
373 }
374
375 let payload = unsafe {
376 parse_optional_json_ptr::<VerificationResultPayload<TransactionPayload>>(
377 result_json,
378 "current entitlement transaction",
379 )
380 }?;
381 payload
382 .map(|payload| {
383 payload.into_result(|payload| Self::from_raw_parts(transaction_handle, payload))
384 })
385 .transpose()
386 }
387
388 pub const fn data(&self) -> &TransactionData {
390 &self.data
391 }
392
393 pub const fn advanced_commerce_info(&self) -> Option<&TransactionAdvancedCommerceInfo> {
395 self.advanced_commerce_info.as_ref()
396 }
397
398 pub const fn has_live_handle(&self) -> bool {
400 self.handle.is_some()
401 }
402
403 pub fn verify(&self) -> Result<(), StoreKitError> {
405 self.handle.map_or_else(
406 || {
407 self.data
408 .verification_failure
409 .clone()
410 .map_or(Ok(()), |failure| Err(StoreKitError::Verification(failure)))
411 },
412 |handle| {
413 let mut error_message = ptr::null_mut();
414 let status =
415 unsafe { ffi::sk_transaction_verify(handle.as_ptr(), &mut error_message) };
416 if status == ffi::status::OK {
417 Ok(())
418 } else {
419 Err(unsafe { error_from_status(status, error_message) })
420 }
421 },
422 )
423 }
424
425 pub fn finish(&self) -> Result<(), StoreKitError> {
427 self.handle.map_or_else(
428 || {
429 Err(StoreKitError::NotSupported(
430 "transaction snapshots cannot be finished because they do not carry a live StoreKit handle"
431 .to_owned(),
432 ))
433 },
434 |handle| {
435 let mut error_message = ptr::null_mut();
436 let status = unsafe { ffi::sk_transaction_finish(handle.as_ptr(), &mut error_message) };
437 if status == ffi::status::OK {
438 Ok(())
439 } else {
440 Err(unsafe { error_from_status(status, error_message) })
441 }
442 },
443 )
444 }
445
446 pub fn begin_refund_request(&self) -> Result<RefundRequestStatus, StoreKitError> {
448 Refund::begin_for_transaction_id(self.data.id)
449 }
450
451 pub(crate) fn from_raw_parts(
452 handle: *mut c_void,
453 payload: TransactionPayload,
454 ) -> Result<Self, StoreKitError> {
455 let (data, advanced_commerce_info) = payload.into_transaction_parts()?;
456 Ok(Self {
457 handle: NonNull::new(handle),
458 data,
459 advanced_commerce_info,
460 })
461 }
462
463 pub(crate) fn from_snapshot_payload(
464 payload: TransactionPayload,
465 ) -> Result<Self, StoreKitError> {
466 let (data, advanced_commerce_info) = payload.into_transaction_parts()?;
467 Ok(Self {
468 handle: None,
469 data,
470 advanced_commerce_info,
471 })
472 }
473}
474
475#[derive(Debug)]
476pub struct TransactionStream {
478 handle: NonNull<c_void>,
479 finished: bool,
480}
481
482impl Drop for TransactionStream {
483 fn drop(&mut self) {
484 unsafe { ffi::sk_transaction_stream_release(self.handle.as_ptr()) };
485 }
486}
487
488impl TransactionStream {
489 fn new(config: &TransactionStreamConfig) -> Result<Self, StoreKitError> {
490 let config_json = json_cstring(config, "transaction stream config")?;
491 let mut error_message = ptr::null_mut();
492 let handle =
493 unsafe { ffi::sk_transaction_stream_create(config_json.as_ptr(), &mut error_message) };
494 let handle = NonNull::new(handle)
495 .ok_or_else(|| unsafe { error_from_status(ffi::status::UNKNOWN, error_message) })?;
496 Ok(Self {
497 handle,
498 finished: false,
499 })
500 }
501
502 pub const fn is_finished(&self) -> bool {
504 self.finished
505 }
506
507 #[allow(clippy::should_implement_trait)]
508 pub fn next(&mut self) -> Result<Option<VerificationResult<Transaction>>, StoreKitError> {
510 self.next_timeout(Duration::from_secs(30))
511 }
512
513 pub fn next_timeout(
515 &mut self,
516 timeout: Duration,
517 ) -> Result<Option<VerificationResult<Transaction>>, StoreKitError> {
518 let mut transaction_handle = ptr::null_mut();
519 let mut verification_json = ptr::null_mut();
520 let mut error_message = ptr::null_mut();
521 let status = unsafe {
522 ffi::sk_transaction_stream_next(
523 self.handle.as_ptr(),
524 duration_to_timeout_ms(timeout),
525 &mut transaction_handle,
526 &mut verification_json,
527 &mut error_message,
528 )
529 };
530
531 match status {
532 ffi::status::OK => {
533 let payload = unsafe {
534 parse_json_ptr::<VerificationResultPayload<TransactionPayload>>(
535 verification_json,
536 "transaction verification result",
537 )
538 };
539 match payload {
540 Ok(payload) => payload
541 .into_result(|payload| {
542 Transaction::from_raw_parts(transaction_handle, payload)
543 })
544 .map(Some),
545 Err(error) => {
546 if !transaction_handle.is_null() {
547 unsafe { ffi::sk_transaction_release(transaction_handle) };
548 }
549 Err(error)
550 }
551 }
552 }
553 ffi::status::END_OF_STREAM => {
554 self.finished = true;
555 Ok(None)
556 }
557 ffi::status::TIMED_OUT => Ok(None),
558 _ => Err(unsafe { error_from_status(status, error_message) }),
559 }
560 }
561}
562
563#[derive(Debug, Serialize)]
564struct TransactionStreamConfig {
565 kind: &'static str,
566 #[serde(rename = "productID", skip_serializing_if = "Option::is_none")]
567 product_id: Option<String>,
568}
569
570impl TransactionStreamConfig {
571 const fn all() -> Self {
572 Self {
573 kind: "all",
574 product_id: None,
575 }
576 }
577
578 const fn current_entitlements() -> Self {
579 Self {
580 kind: "currentEntitlements",
581 product_id: None,
582 }
583 }
584
585 const fn updates() -> Self {
586 Self {
587 kind: "updates",
588 product_id: None,
589 }
590 }
591
592 const fn unfinished() -> Self {
593 Self {
594 kind: "unfinished",
595 product_id: None,
596 }
597 }
598
599 fn all_for(product_id: &str) -> Self {
600 Self {
601 kind: "allFor",
602 product_id: Some(product_id.to_owned()),
603 }
604 }
605
606 fn current_entitlements_for(product_id: &str) -> Self {
607 Self {
608 kind: "currentEntitlementsFor",
609 product_id: Some(product_id.to_owned()),
610 }
611 }
612}
613
614#[derive(Debug, Deserialize)]
615pub(crate) struct TransactionOfferPayload {
616 id: Option<String>,
617 #[serde(rename = "type")]
618 offer_type: String,
619 #[serde(rename = "paymentMode")]
620 payment_mode: Option<String>,
621 period: Option<SubscriptionPeriodPayload>,
622}
623
624impl TransactionOfferPayload {
625 pub(crate) fn into_transaction_offer(self) -> TransactionOffer {
626 TransactionOffer {
627 id: self.id,
628 offer_type: OfferType::from_raw(self.offer_type),
629 payment_mode: self.payment_mode.map(OfferPaymentMode::from_raw),
630 period: self
631 .period
632 .map(SubscriptionPeriodPayload::into_subscription_period),
633 }
634 }
635}
636
637#[derive(Debug, Deserialize)]
638pub(crate) struct TransactionPayload {
639 id: u64,
640 #[serde(rename = "originalID")]
641 original_id: u64,
642 #[serde(rename = "webOrderLineItemID")]
643 web_order_line_item_id: Option<String>,
644 #[serde(rename = "productID")]
645 product_id: String,
646 #[serde(rename = "subscriptionGroupID")]
647 subscription_group_id: Option<String>,
648 #[serde(rename = "appBundleID")]
649 app_bundle_id: String,
650 #[serde(rename = "purchaseDate")]
651 purchase_date: String,
652 #[serde(rename = "originalPurchaseDate")]
653 original_purchase_date: String,
654 #[serde(rename = "expirationDate")]
655 expiration_date: Option<String>,
656 #[serde(rename = "purchasedQuantity")]
657 purchased_quantity: u64,
658 #[serde(rename = "isUpgraded")]
659 is_upgraded: bool,
660 #[serde(rename = "ownershipType")]
661 ownership_type: String,
662 #[serde(rename = "signedDate")]
663 signed_date: String,
664 #[serde(rename = "jwsRepresentation")]
665 jws_representation: String,
666 #[serde(rename = "verificationError")]
667 verification_error: Option<crate::error::VerificationErrorPayload>,
668 #[serde(rename = "revocationDate")]
669 revocation_date: Option<String>,
670 #[serde(rename = "revocationReason")]
671 revocation_reason: Option<String>,
672 #[serde(rename = "productType")]
673 product_type: Option<String>,
674 #[serde(rename = "appAccountToken")]
675 app_account_token: Option<String>,
676 environment: Option<String>,
677 reason: Option<String>,
678 storefront: Option<StorefrontPayload>,
679 price: Option<String>,
680 #[serde(rename = "currencyCode")]
681 currency_code: Option<String>,
682 #[serde(rename = "appTransactionID")]
683 app_transaction_id: Option<String>,
684 offer: Option<TransactionOfferPayload>,
685 #[serde(rename = "advancedCommerceInfo")]
686 advanced_commerce_info: Option<TransactionAdvancedCommerceInfoPayload>,
687 #[serde(rename = "jsonRepresentationBase64")]
688 json_representation_base64: String,
689}
690
691impl TransactionPayload {
692 fn into_transaction_parts(
693 self,
694 ) -> Result<(TransactionData, Option<TransactionAdvancedCommerceInfo>), StoreKitError> {
695 let advanced_commerce_info = self
696 .advanced_commerce_info
697 .map(TransactionAdvancedCommerceInfoPayload::into_transaction_advanced_commerce_info);
698 Ok((
699 TransactionData {
700 id: self.id,
701 original_id: self.original_id,
702 web_order_line_item_id: self.web_order_line_item_id,
703 product_id: self.product_id,
704 subscription_group_id: self.subscription_group_id,
705 app_bundle_id: self.app_bundle_id,
706 purchase_date: self.purchase_date,
707 original_purchase_date: self.original_purchase_date,
708 expiration_date: self.expiration_date,
709 purchased_quantity: self.purchased_quantity,
710 is_upgraded: self.is_upgraded,
711 ownership_type: OwnershipType::from_raw(self.ownership_type),
712 signed_date: self.signed_date,
713 jws_representation: self.jws_representation,
714 verification_failure: self
715 .verification_error
716 .map(crate::error::VerificationFailure::from_payload),
717 revocation_date: self.revocation_date,
718 revocation_reason: self.revocation_reason.map(RevocationReason::from_raw),
719 product_type: self.product_type.map(ProductType::from_raw),
720 app_account_token: self.app_account_token,
721 environment: self.environment.map(AppStoreEnvironment::from_raw),
722 reason: self.reason.map(TransactionReason::from_raw),
723 storefront: self.storefront.map(StorefrontPayload::into_storefront),
724 price: self.price,
725 currency_code: self.currency_code,
726 app_transaction_id: self.app_transaction_id,
727 offer: self
728 .offer
729 .map(TransactionOfferPayload::into_transaction_offer),
730 json_representation: decode_base64(
731 &self.json_representation_base64,
732 "transaction JSON representation",
733 )?,
734 },
735 advanced_commerce_info,
736 ))
737 }
738}