{"id":78314,"date":"2026-03-13T16:44:57","date_gmt":"2026-03-13T11:14:57","guid":{"rendered":"https:\/\/www.tothenew.com\/blog\/?p=78314"},"modified":"2026-03-17T13:14:03","modified_gmt":"2026-03-17T07:44:03","slug":"react-native-iap-%c2%b7-storekit-storekit-2-the-definitive-ios-guide","status":"publish","type":"post","link":"https:\/\/www.tothenew.com\/blog\/react-native-iap-%c2%b7-storekit-storekit-2-the-definitive-ios-guide\/","title":{"rendered":"REACT NATIVE IAP \u00b7 StoreKit &amp; StoreKit 2 &#8211; The Definitive iOS Guide"},"content":{"rendered":"<h2>Why This Matters for React Native IAP Developers<\/h2>\n<p>If you have shipped iOS purchases with react-native-iap, you know the ritual: Base64 receipt blobs, global listeners scattered across files, flaky Sandbox testers, and a backend endpoint Apple is actively shutting down. StoreKit 2 is not a patch \u2014 it is a clean architectural rewrite. Apple rebuilt iOS payments around async\/await, replacing every delegate callback with a promise and every opaque receipt with a cryptographically signed JWS (JSON Web Signature) token you can verify on-device without a network call. In react-native-iap v12+ you unlock the entire new model with one config line: storekitMode: &#8220;STOREKIT2_MODE&#8221;.<\/p>\n<div id=\"attachment_78312\" style=\"width: 954px\" class=\"wp-caption aligncenter\"><img aria-describedby=\"caption-attachment-78312\" decoding=\"async\" loading=\"lazy\" class=\"wp-image-78312 size-full\" src=\"https:\/\/www.tothenew.com\/blog\/wp-ttn-blog\/uploads\/2026\/03\/Screenshot-2026-03-10-at-11.31.29\u202fPM.png\" alt=\"na\" width=\"944\" height=\"412\" srcset=\"\/blog\/wp-ttn-blog\/uploads\/2026\/03\/Screenshot-2026-03-10-at-11.31.29\u202fPM.png 944w, \/blog\/wp-ttn-blog\/uploads\/2026\/03\/Screenshot-2026-03-10-at-11.31.29\u202fPM-300x131.png 300w, \/blog\/wp-ttn-blog\/uploads\/2026\/03\/Screenshot-2026-03-10-at-11.31.29\u202fPM-768x335.png 768w, \/blog\/wp-ttn-blog\/uploads\/2026\/03\/Screenshot-2026-03-10-at-11.31.29\u202fPM-624x272.png 624w\" sizes=\"(max-width: 944px) 100vw, 944px\" \/><p id=\"caption-attachment-78312\" class=\"wp-caption-text\">SK1 (left): delegate\/observer cascade ending at the deprecated \/verifyReceipt endpoint. SK2 (right): async\/await chain with self-verifiable JWS tokens and the current App Store Server API.<\/p><\/div>\n<h2>Key Architectural Changes<\/h2>\n<ul>\n<li style=\"list-style-type: none;\">\n<ul>\n<li><strong>Transactions are now first-class citizens.<\/strong> Instead of a single monolithic receipt, SK2 gives you individual Transaction objects \u2014 signed in JSON Web Signature (JWS) format. Each Transaction carries its own payload: product ID, purchase date, expiry date, quantity, and crucially, a cryptographic signature you can verify on-device without a network call. This alone eliminates the biggest operational risk in SK1.<\/li>\n<li><strong>Async\/await throughout.<\/strong> Every SK2 API uses Swift concurrency. Product.products(for:), Product.purchase(), Transaction.currentEntitlements \u2014 all return with await. React Native bridges this naturally through the react-native-iap promise layer, giving you clean async\/await in JavaScript.<\/li>\n<li><strong>Transaction.currentEntitlements.<\/strong> This async sequence delivers every currently-active entitlement for the user \u2014 all active subscriptions, all unconsumed consumables, all non-consumables. One call, correct state, no receipt parsing.<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n<h2>JWS vs Receipt \u2014 Why Your Backend Changes Too<\/h2>\n<ul>\n<li><strong>SK1 backend:<\/strong> You POST a Base64 receipt blob to \/verifyReceipt, parse a deeply nested JSON response, find latest_receipt_info, check expires_date_ms, and grant access. This endpoint is deprecated \u2014 Apple will remove it.<\/li>\n<li><strong>SK2 backend:<\/strong> Each transaction delivers a jwsRepresentation string \u2014 a standard ES256-signed JWT. Verify the signature against Apple&#8217;s public key at appleid.apple.com\/auth\/keys. The payload is plain typed JSON: productId, expiresDate, type. No parsing gymnastics. Verifiable on-device or on your server.<\/li>\n<\/ul>\n<h2>Code-level changes in react-native-iap when migrating from SK1 to SK2<\/h2>\n<ul>\n<li>Initialisation &amp; Setup<br \/>\n<table style=\"border-collapse: collapse; width: 100%;\">\n<tbody>\n<tr>\n<td style=\"width: 50%;\">StoreKit 1<br \/>\nimport { setup } from &#8216;react-native-iap&#8217;;<br \/>\nsetup({ storekitMode: &#8216;STOREKIT1_MODE&#8217; });<\/td>\n<td style=\"width: 50%;\">StoreKit 2<br \/>\nimport { setup } from &#8216;react-native-iap&#8217;;<br \/>\nsetup({ storekitMode: &#8216;STOREKIT1_MODE&#8217; });<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/li>\n<li>Fetching Products<br \/>\n<table style=\"height: 564px; width: 90%; border-collapse: collapse;\">\n<tbody>\n<tr>\n<td style=\"width: 50%;\">\n<p style=\"text-align: left;\"><strong>StoreKit 1<\/strong><\/p>\n<p style=\"text-align: left;\">\/\/ getProducts for consumables \/<\/p>\n<p style=\"text-align: left;\">\/\/ non-consumables<\/p>\n<p style=\"text-align: left;\">const products = await getProducts({<\/p>\n<p style=\"text-align: left;\">\u00a0 skus: [&#8216;com.app.coins&#8217;],<\/p>\n<p style=\"text-align: left;\">});<\/p>\n<p style=\"text-align: left;\">\/\/ getSubscriptions for subs<\/p>\n<p style=\"text-align: left;\">const subs = await getSubscriptions({<\/p>\n<p style=\"text-align: left;\">\u00a0 skus: [&#8216;com.app.monthly&#8217;],<\/p>\n<p style=\"text-align: left;\">});<\/p>\n<\/td>\n<td style=\"width: 50%; text-align: left;\"><strong>StoreKit 2\u00a0<\/strong>\/\/ Same API \u2014 no change needed<\/p>\n<p>\/\/ SK2 resolves product metadata<\/p>\n<p>const products = await getProducts({<\/p>\n<p>skus: [&#8216;com.app.coins&#8217;],<\/p>\n<p>});<\/p>\n<p>&nbsp;<\/p>\n<p>const subs = await getSubscriptions({<\/p>\n<p>skus: [&#8216;com.app.monthly&#8217;],<\/p>\n<p>});<\/p>\n<p>&nbsp;<\/p>\n<p>\/\/ \u2713 No code change required here<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/li>\n<li>Purchase Flow\u00a0 \u2190 Biggest Change<br \/>\n<table style=\"width: 90%; border-collapse: collapse;\">\n<tbody>\n<tr>\n<td style=\"width: 50%;\"><strong>StoreKit 1\u00a0 \u2014\u00a0 Listener \/ Callback<\/strong><\/p>\n<p>\/\/ Must register GLOBALLY<\/p>\n<p>\/\/ before any purchase attempt<\/p>\n<p>const sub = purchaseUpdatedListener(<\/p>\n<p>async (purchase) =&gt; {<\/p>\n<p>if (purchase.transactionReceipt){<\/p>\n<p>await sendToServer(<\/p>\n<p>purchase.transactionReceipt<\/p>\n<p>\/\/ \u2191 raw Base64 blob<\/p>\n<p>);<\/p>\n<p>await finishTransaction(<\/p>\n<p>{ purchase });<\/p>\n<p>}<\/p>\n<p>});<\/p>\n<p>&nbsp;<\/p>\n<p>\/\/ Trigger purchase separately<\/p>\n<p>await requestPurchase({ sku });<\/p>\n<p>&nbsp;<\/p>\n<p>\/\/ \u26a0 Must remove \u2014 leaks if missed<\/p>\n<p>sub.remove();<\/td>\n<td style=\"width: 50%;\"><strong>StoreKit 2\u00a0 \u2014\u00a0 Async \/ Await<\/strong><\/p>\n<p>\/\/ No listener needed at all<\/p>\n<p>\/\/ requestPurchase returns a promise<\/p>\n<p>&nbsp;<\/p>\n<p>try {<\/p>\n<p>const purchase =<\/p>\n<p>await requestPurchase({<\/p>\n<p>sku,<\/p>\n<p>andDangerously<\/p>\n<p>FinishTransaction<\/p>\n<p>AutomaticallyIOS: false,<\/p>\n<p>});<\/p>\n<p>&nbsp;<\/p>\n<p>\/\/ transactionId = JWS token<\/p>\n<p>await verifyJWS(<\/p>\n<p>purchase.transactionId<\/p>\n<p>\/\/ \u2191 signed JWT, not Base64<\/p>\n<p>);<\/p>\n<p>await finishTransaction(<\/p>\n<p>{ purchase });<\/p>\n<p>} catch (e) { \/* handle *\/ }<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/li>\n<\/ul>\n<h2 style=\"text-align: left;\"><strong>Summary<\/strong><\/h2>\n<p style=\"text-align: left;\">StoreKit 1 served the iOS developer community for fifteen years and deserves respect for enabling an entire economy of premium apps and subscriptions. But its delegate-based, receipt-centric model was always a leaky abstraction \u2014 one that required significant server infrastructure and constant maintenance just to answer a simple question: &#8220;does this user have an active subscription?&#8221;<\/p>\n<p style=\"text-align: left;\">StoreKit 2 answers that question in three lines of Swift. It eliminates receipt parsing, replaces observer spaghetti with clean async\/await, provides on-device cryptographic verification, and ships with an Xcode-integrated test harness that makes TDD for in-app purchases genuinely possible for the first time.<\/p>\n<p style=\"text-align: left;\">For React Native developers, react-native-iap v12+ exposes all of this through a promise-based API that feels natural. The migration from SK1 to SK2 is non-trivial \u2014 especially if you have an existing backend validation layer \u2014 but the operational benefits are immediate and compounding. Every new Apple feature from here forward will be SK2 only. The time to migrate is now.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Why This Matters for React Native IAP Developers If you have shipped iOS purchases with react-native-iap, you know the ritual: Base64 receipt blobs, global listeners scattered across files, flaky Sandbox testers, and a backend endpoint Apple is actively shutting down. StoreKit 2 is not a patch \u2014 it is a clean architectural rewrite. Apple rebuilt [&hellip;]<\/p>\n","protected":false},"author":2247,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"iawp_total_views":60},"categories":[5881],"tags":[8458,8459,8460],"aioseo_notices":[],"_links":{"self":[{"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/posts\/78314"}],"collection":[{"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/users\/2247"}],"replies":[{"embeddable":true,"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/comments?post=78314"}],"version-history":[{"count":4,"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/posts\/78314\/revisions"}],"predecessor-version":[{"id":78646,"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/posts\/78314\/revisions\/78646"}],"wp:attachment":[{"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/media?parent=78314"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/categories?post=78314"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/tags?post=78314"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}