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