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