# PurchaseKit > In-app purchase infrastructure for Rails apps with Hotwire Native. Handles App Store and Google Play webhooks, normalizes subscription data, and syncs to your Rails app. - Website: https://purchasekit.com - Documentation: https://purchasekit.com/docs --- ## Getting started
PurchaseKit enables in-app purchases for Hotwire Native apps built with Rails.
See a working in-app purchase in under 10 minutes. No PurchaseKit account or Apple Developer account needed.
The demo runs entirely locally using Xcode's StoreKit Configuration. You'll make a test purchase and see how PurchaseKit handles the entire flow. ### Prerequisites - macOS with Xcode 26+ - Ruby 3.2+ with Bundler ### 1. Clone the repo ```bash git clone https://github.com/purchasekit/purchasekit-demo.git cd purchasekit-demo ``` ### 2. Start the Rails server ```bash cd demo/rails/pay bin/setup ``` The server starts on `http://localhost:3000`. Leave this terminal running. ### 3. Run the iOS app 1. Open `demo/ios/PurchaseKitDemo.xcodeproj` in Xcode 2. Select the **PurchaseKitDemo** scheme and an iPhone simulator 3. Press **Cmd+R** to build and run The app connects to your local Rails server automatically. ### 4. Make a test purchase 1. Sign in with `user@example.com` / `password` 2. Tap the **Subscribe** button on the paywall 3. Confirm the purchase in the StoreKit dialog 4. Watch the app redirect to the success page That's it. You just completed an in-app purchase flow! ### What just happened? 1. The Rails app rendered an HTML paywall 2. Hotwire Native displayed it in the iOS app 3. You tapped subscribe, triggering StoreKit 4. The gem's demo mode simulated the webhook locally 5. A `Pay::Subscription` record was created 6. The user was redirected to the success page In production, steps 4-5 happen via PurchaseKit's webhook infrastructure instead of locally. ### Next steps Ready to add purchases to your app? 1. [Create an account](/registration/new) on PurchaseKit 2. Follow the [Getting started guide](/docs) --- ## Setup with PayAdd in-app purchases to your Rails app with the purchasekit gem.
Use the purchasekit gem without Pay for custom subscription handling.
Build an HTML paywall that triggers native in-app purchases.
### Create a paywall Fetch products in your controller: ```ruby class PaywallsController < ApplicationController def show @annual = PurchaseKit::Product.find("prod_XXXXXXXX") @monthly = PurchaseKit::Product.find("prod_YYYYYYYY") end end ``` You'll find product IDs in the PurchaseKit dashboard under **Apps → [Your App] → Products**. #### Minimal example ```erb <%= turbo_stream_from current_user.payment_processor %> <%= purchasekit_paywall customer_id: current_user.payment_processor.id, success_path: dashboard_path do |paywall| %> <%= paywall.plan_option product: @annual, selected: true do %> Annual - <%= paywall.price %>/year <% end %> <%= paywall.plan_option product: @monthly do %> Monthly - <%= paywall.price %>/month <% end %> <%= paywall.submit "Subscribe" %> <% end %> <%= button_to "Restore purchases", restore_purchases_path %> ``` #### Styled example (Bootstrap) ```erb <%= turbo_stream_from current_user.payment_processor %> <%= purchasekit_paywall customer_id: current_user.payment_processor.id, success_path: dashboard_path do |paywall| %>Add the PurchaseKit Swift package to your Hotwire Native iOS app.
### Installation Add the package via Swift Package Manager in Xcode: 1. Open your Xcode project 2. Go to **File → Add Package Dependencies** 3. Enter the package URL: ``` https://github.com/purchasekit/purchasekit-ios ``` 4. Select the latest version and click **Add Package**  ### Register the bridge component In your app's setup code, register the `PaywallComponent` with Hotwire Native: ```swift{8-10} import HotwireNative import PurchaseKit import UIKit @main class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { Hotwire.registerBridgeComponents([ PaywallComponent.self ]) return true } } ``` That's it! The component automatically: - Listens for price requests from your web paywall - Handles StoreKit purchases when the user subscribes - Finishes transactions (fulfillment happens via webhooks) ### Environment detection PurchaseKit automatically detects whether the app is running in sandbox or production: | Build type | Environment | |------------|-------------| | Simulator | sandbox | | Development build | sandbox | | TestFlight | sandbox | | App Store | production | This ensures sandbox purchases don't accidentally get treated as production (and vice versa). ### Requirements - iOS 16.0+ - Xcode 15.0+ - Hotwire Native iOS 1.2.0+ ### Next step [Create your subscriptions](ios/subscriptions) in App Store Connect. --- ## Creating subscriptions in App Store ConnectCreate the products users will purchase. PurchaseKit uses these product IDs to match purchases to your app.
### Create a subscription group Subscription groups let users upgrade or downgrade between plans without being charged twice. 1. In App Store Connect, go to your app 2. Select **Subscriptions** in the sidebar 3. Click the **+** button next to **Subscription Groups** 4. Enter a reference name (e.g., "Pro Plans")  ### Add a subscription 1. Within your subscription group, click **Create** next to **Subscriptions** 2. Enter a **Reference Name** (internal only, e.g., "Pro Annual") 3. Enter a **Product ID** - this is what you'll use in PurchaseKit: - Use reverse domain notation: `com.yourapp.pro.annual` - Be consistent: `com.yourapp.pro.monthly`, `com.yourapp.pro.annual`  4. Click **Create** 5. Configure subscription details: - **Subscription Duration** (1 week, 1 month, 1 year, etc.) - **Availability** → select which countries can purchase - **Subscription Prices** → click **Add Subscription Price** ### Add the product to PurchaseKit 1. In the PurchaseKit dashboard, go to your app 2. Click **Add a product** 3. Enter the Apple Product ID (e.g., `com.yourapp.pro.annual`) 4. Save the product The product ID in PurchaseKit must exactly match the Product ID in App Store Connect. ### Next step Configure [App Store Connect](ios/app-store-connect) to send webhooks to PurchaseKit. --- ## App Store Connect webhooksConnect App Store Connect to PurchaseKit so you receive real-time subscription updates (renewals, cancellations, refunds).
### Bundle identifier The bundle identifier connects your iOS app to App Store Connect and PurchaseKit. #### Find your bundle ID in Xcode 1. Open your Xcode project 2. Select your app target 3. Go to the **Signing & Capabilities** tab 4. Find **Bundle Identifier**  #### Verify in App Store Connect 1. In App Store Connect, select your app 2. Go to **App Information** 3. Confirm the **Bundle ID** matches your Xcode project #### Configure in PurchaseKit 1. In the PurchaseKit dashboard, go to your app settings 2. Enter your **Apple Bundle ID** 3. Save changes PurchaseKit uses the bundle ID to match incoming Apple webhooks to the correct app. ### Server Notifications Apple sends server notifications when subscriptions are created, renewed, or canceled. PurchaseKit receives these webhooks and forwards them to your Rails app. 1. Open [App Store Connect](https://appstoreconnect.apple.com) 2. Select your app 3. Navigate to **App Information** in the sidebar 4. Scroll to **App Store Server Notifications**  5. For **Production Server URL**, enter: ``` https://purchasekit.com/webhooks/apple ``` 6. For **Sandbox Server URL**, enter the same URL (PurchaseKit handles both environments) 7. Click **Save** ### Next step [Test locally](ios/testing) with StoreKit Configuration files. --- ## Testing on iOSTest your PurchaseKit integration locally without receiving real Apple webhooks.
### StoreKit Configuration files Xcode's StoreKit Configuration files let you test purchases in the simulator without connecting to App Store Connect. PurchaseKit automatically detects these purchases and completes them locally. #### Create a StoreKit Configuration file 1. In Xcode, go to **File → New → File** 2. Search for "StoreKit" and select **StoreKit Configuration File** 3. Name it (e.g., `StoreKit.storekit`) and save it in your project  #### Sync products from App Store Connect 1. Open your `.storekit` file 2. Check **Sync this file with an app in App Store Connect** 3. Select your app from the dropdown This automatically imports your subscriptions and keeps them in sync. #### Enable the configuration 1. In Xcode, go to **Product → Scheme → Edit Scheme** 2. Select **Run** in the sidebar 3. Go to the **Options** tab 4. Set **StoreKit Configuration** to your `.storekit` file  #### Clear test purchases To reset and test again: 1. In Xcode, go to **Debug → StoreKit → Manage Transactions** 2. Select and delete previous transactions 3. Or use **Debug → StoreKit → Clear Purchase History** ### Testing webhooks locally To test real Apple sandbox webhooks locally: 1. Expose your local Rails app with [Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/do-more-with-tunnels/trycloudflare/): ```bash cloudflared tunnel --url http://localhost:3000 ``` 2. In the PurchaseKit dashboard, set your app's **Sandbox Webhook URL** to your tunnel URL: ``` https://example-tunnel.trycloudflare.com/purchasekit/webhooks ``` 3. Make a sandbox purchase on a real device (not simulator) 4. Watch the webhook arrive in your Rails logs #### Sandbox subscription durations Apple accelerates sandbox subscriptions for faster testing: | Production duration | Sandbox duration | |---------------------|------------------| | 1 week | 3 minutes | | 1 month | 5 minutes | | 2 months | 10 minutes | | 3 months | 15 minutes | | 6 months | 30 minutes | | 1 year | 1 hour | #### Test webhook script The example Rails app includes a script to test webhook handling: ```bash bin/test_webhook created # Test subscription.created bin/test_webhook updated # Test subscription.updated bin/test_webhook canceled # Test subscription.canceled bin/test_webhook expired # Test subscription.expired ``` --- ## Android setupAdd the PurchaseKit library to your Android app and register the bridge component.
### Installation #### Add JitPack repository Add JitPack to your project's `settings.gradle.kts`: ```kotlin{5} dependencyResolutionManagement { repositories { google() mavenCentral() maven { url = uri("https://jitpack.io") } } } ``` #### Add the dependency Add PurchaseKit to your app's `build.gradle.kts`: ```kotlin dependencies { implementation("com.github.purchasekit:purchasekit-android:0.1.0") } ``` ### Register the bridge component In your Application class, register the `PaywallComponent`: ```kotlin{3,12-14} import android.app.Application import dev.hotwire.core.bridge.BridgeComponentFactory import dev.hotwire.core.bridge.KotlinXJsonConverter import dev.hotwire.core.config.Hotwire import dev.hotwire.navigation.config.registerBridgeComponents import dev.purchasekit.android.PaywallComponent class MyApplication : Application() { override fun onCreate() { super.onCreate() Hotwire.registerBridgeComponents( BridgeComponentFactory("paywall", ::PaywallComponent) ) Hotwire.config.jsonConverter = KotlinXJsonConverter() } } ``` The component name `"paywall"` must match the name used in your web app's JavaScript. ### How it works When your web paywall loads, it sends a `prices` message to the native app. PurchaseKit fetches localized prices from Google Play and returns them to the web. When the user taps Subscribe, your web app sends a `purchase` message with: - `googleStoreProductId` - The product ID from Google Play Console - `correlationId` - A UUID that links this purchase to your PurchaseKit Purchase Intent PurchaseKit launches the Google Play purchase flow and returns the result. ### Testing #### License testers Add your test accounts to Play Console → Setup → License testing. License testers can make purchases without being charged. #### Internal testing For full end-to-end testing: 1. Create an internal testing track in Play Console 2. Upload your APK or AAB 3. Add testers and share the opt-in link 4. Testers install via the Play Store ### Next step [Create your subscriptions](android/subscriptions) in Google Play Console. --- ## Creating subscriptions in Google Play ConsoleCreate the products users will purchase. PurchaseKit uses these product IDs to match purchases to your app.
### Create a subscription 1. In [Google Play Console](https://play.google.com/console), select your app 2. Go to **Monetize** → **Subscriptions** 3. Click **Create subscription** 4. Enter a **Product ID** - this is what you'll use in PurchaseKit: - Use reverse domain notation: `com.yourapp.pro.annual` - Be consistent: `com.yourapp.pro.monthly`, `com.yourapp.pro.annual` 5. Enter a name and description 6. Click **Create** ### Add a base plan Each subscription needs at least one base plan: 1. Within your subscription, click **Add base plan** 2. Enter a **Base plan ID** (e.g., `annual`) 3. Set the **Billing period** (weekly, monthly, yearly, etc.) 4. Click **Set price** and configure pricing for each country 5. Click **Activate** ### Add the product to PurchaseKit 1. In the PurchaseKit dashboard, go to your app 2. Click **Add a product** 3. Enter the Google Product ID (e.g., `com.yourapp.pro.annual`) 4. Save the product The product ID in PurchaseKit must exactly match the Product ID in Google Play Console. ### Next step Configure [Google Play Console](android/google-play-console) to send webhooks to PurchaseKit. --- ## Google Play Console webhooksConnect Google Play to PurchaseKit so you receive real-time subscription updates (renewals, cancellations, refunds).
### Package name The package name connects your Android app to Google Play Console and PurchaseKit. #### Find your package name 1. Open your Android Studio project 2. Open `app/build.gradle.kts` (or `build.gradle`) 3. Find the `applicationId` in the `android` block: ```kotlin android { namespace = "com.yourapp.android" defaultConfig { applicationId = "com.yourapp.android" } } ``` #### Configure in PurchaseKit 1. In the PurchaseKit dashboard, go to your app settings 2. Enter your **Google Package Name** 3. Save changes PurchaseKit uses the package name to match incoming Google webhooks to the correct app. ### Automated setup (recommended) Run our script in Google Cloud Shell to configure the Pub/Sub topic and service account. #### Open Google Cloud Shell 1. Go to [Google Cloud Console](https://console.cloud.google.com) 2. Select your project from the dropdown at the top 3. Click the **Cloud Shell** icon (terminal icon) in the top-right toolbar 4. Wait for the shell to initialize #### Create and run the script 1. In Cloud Shell, click **Open Editor** (pencil icon) to open the editor 2. Click **File → New File** 3. Copy the script from [purchasekit.com/scripts/google-setup.sh](/scripts/google-setup.sh) and paste it 4. Change line 18: replace `your_project_id` with your Google Cloud project ID 5. Click **File → Save As** and name it `setup.sh` 6. Click **Open Terminal** to return to the terminal 7. Run the script: ``` bash setup.sh ```  After the script completes: 1. [Add the service account to Play Console](#link-to-google-play-console) 2. [Enable real-time notifications](#enable-real-time-notifications) in your app 3. [Upload the JSON key](https://purchasekit.com/account/developer) to PurchaseKit --- ### Manual setup If you prefer to configure everything manually, follow the steps below. #### Google Cloud project 1. Go to [Google Cloud Console](https://console.cloud.google.com) 2. Click the project dropdown at the top 3. Click **New Project** 4. Enter a name (e.g., "My App Purchases") 5. Click **Create** #### Enable APIs 1. In Google Cloud Console, go to **APIs & Services** → **Library** 2. Search for and enable: - **Cloud Pub/Sub API** - **Google Play Developer API** #### Create a Pub/Sub topic 1. Go to **Pub/Sub** → **Topics** 2. Click **Create Topic** 3. Enter a topic ID (e.g., `purchasekit-play-notifications`) 4. Uncheck "Add a default subscription" 5. Click **Create** #### Create a push subscription 1. Click on your newly created topic 2. Click **Create Subscription** 3. Enter a subscription ID (e.g., `purchasekit-push`) 4. Set **Delivery type** to **Push** 5. Enter the PurchaseKit webhook URL: ``` https://purchasekit.com/webhooks/google ``` 6. Click **Create** #### Grant Pub/Sub permissions Google Play needs permission to publish to your topic: 1. Go to **Pub/Sub** → **Topics** 2. Click on your topic 3. Click the **Permissions** tab 4. Click **Grant Access** 5. For **New principals**, enter: ``` google-play-developer-notifications@system.gserviceaccount.com ``` 6. For **Role**, select **Pub/Sub Publisher** 7. Click **Save** #### Create a service account PurchaseKit needs a service account to fetch subscription details from Google Play. 1. In Google Cloud Console, go to **IAM & Admin** → **Service Accounts** 2. Click **Create Service Account** 3. Enter a name (e.g., "PurchaseKit") 4. Click **Create and Continue** 5. Skip the optional steps and click **Done** #### Generate a key 1. Click on your new service account 2. Go to the **Keys** tab 3. Click **Add Key** → **Create new key** 4. Select **JSON** 5. Click **Create** 6. Save the downloaded file securely #### Link to Google Play Console 1. Open [Google Play Console](https://play.google.com/console) 2. Go to **Users and permissions** (in the left sidebar, under Setup) 3. Click **Invite new users** 4. Enter the service account email (looks like `name@project.iam.gserviceaccount.com`) 5. Set permissions: - **Account permissions**: View app information and download bulk reports - **Financial data**: View financial data, orders, and cancellation survey responses 6. Click **Invite user** 7. Click **Apply** on the access level page **Note:** Permissions can take up to 24 hours to propagate. #### Upload to PurchaseKit 1. In the PurchaseKit dashboard, go to **Account** → **Developer** 2. Upload the JSON key file you downloaded 3. Click **Upload credentials** #### Enable real-time notifications 1. In Google Play Console, select your app 2. Go to **Monetize** → **Monetization setup** 3. Scroll to **Real-time developer notifications** 4. Enter your Cloud Pub/Sub topic name: ``` projects/YOUR_PROJECT_ID/topics/purchasekit-play-notifications ``` (Replace `YOUR_PROJECT_ID` with your Google Cloud project ID) 5. Click **Save changes** #### Test the connection 1. Click **Send test notification** 2. Check the PurchaseKit dashboard for the incoming webhook 3. If no webhook appears, verify: - Pub/Sub subscription URL is correct - Pub/Sub Publisher permission is granted - Service account permissions have propagated (wait 24 hours) ### Next step Return to [Getting started](/docs) to complete your PurchaseKit integration, or see [Android setup](android/setup) if you haven't added the PurchaseKit library yet. --- ## Testing on AndroidTest your PurchaseKit integration with license testers and internal testing tracks.
### License testers License testers can make purchases without being charged. This is the easiest way to test during development. #### Add license testers 1. Open [Google Play Console](https://play.google.com/console) 2. Go to **Setup → License testing** 3. Add the email addresses of your test accounts 4. Click **Save changes** License testers must use the same Google account on their test device. ### Internal testing track For full end-to-end testing with real Play Store downloads: 1. In Play Console, go to **Testing → Internal testing** 2. Click **Create new release** 3. Upload your APK or AAB 4. Click **Save** and then **Review release** 5. Click **Start rollout to Internal testing** #### Add testers 1. Go to **Testing → Internal testing → Testers** 2. Create a new email list or use an existing one 3. Add tester email addresses 4. Copy the **Join on the web** link and share with testers Testers must accept the invite and install from the Play Store to test purchases. ### Testing webhooks locally To test real Google Play webhooks locally: 1. Expose your local Rails app with [Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/do-more-with-tunnels/trycloudflare/): ```bash cloudflared tunnel --url http://localhost:3000 ``` 2. In the PurchaseKit dashboard, set your app's **Sandbox Webhook URL** to your tunnel URL: ``` https://example-tunnel.trycloudflare.com/purchasekit/webhooks ``` 3. Make a test purchase with a license tester account 4. Watch the webhook arrive in your Rails logs ### Test vs production purchases Google Play doesn't have a separate sandbox environment like Apple. Instead: - **License testers** make test purchases that don't charge their payment method - **Non-testers** make real purchases that are charged PurchaseKit detects test purchases via Google's `testPurchase` flag and marks them as sandbox environment. --- ## Purchase intentsTrack purchases from "user tapped subscribe" to "subscription created."
### View purchase intents In the PurchaseKit dashboard: 1. Go to your app 2. Select a product 3. Click **Purchase intents** You'll see all intents with their status: - **Pending** - Waiting for store webhook - **Completed** - Subscription created successfully ### Manually complete an intent For sandbox testing, you can manually complete a pending intent: 1. Find the pending intent in the dashboard 2. Click **Complete** This simulates receiving a webhook from the app store and triggers your Rails app's webhook handler. The user will be redirected to their success path. > **Note:** Manual completion is only available for sandbox intents. Production intents must come from real Apple or Google webhooks. ### Intent fields | Field | Description | |-------|-------------| | `identifier` | Public ID (e.g., `pi_QCCWGR8F`) | | `uuid` | Correlation ID sent to the store (Apple's `appAccountToken` or Google's `obfuscatedAccountId`) | | `customer_id` | Your `Pay::Customer` ID | | `success_path` | Redirect destination after purchase | | `status` | `pending` or `completed` | | `environment` | `sandbox`, `production`, or `xcode` |