npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@capgo/native-purchases

v6.0.27

Published

In-app Subscriptions Made Easy

Downloads

988

Readme

native-purchases

In-app Purchases Made Easy

This plugin allows you to implement in-app purchases and subscriptions in your Capacitor app using native APIs.

Install

npm install @capgo/native-purchases
npx cap sync

Android

Add this to manifest

<uses-permission android:name="com.android.vending.BILLING" />

Usage

Import the plugin in your TypeScript file:

import { NativePurchases } from '@capgo/native-purchases';

Check if billing is supported

Before attempting to make purchases, check if billing is supported on the device: We only support Storekit 2 on iOS (iOS 15+) and google play on Android

const checkBillingSupport = async () => {
  try {
    const { isBillingSupported } = await NativePurchases.isBillingSupported();
    if (isBillingSupported) {
      console.log('Billing is supported on this device');
    } else {
      console.log('Billing is not supported on this device');
    }
  } catch (error) {
    console.error('Error checking billing support:', error);
  }
};

Get available products

Retrieve information about available products:

const getAvailableProducts = async () => {
  try {
    const { products } = await NativePurchases.getProducts({
      productIdentifiers: ['product_id_1', 'product_id_2'],
      productType: PURCHASE_TYPE.INAPP // or PURCHASE_TYPE.SUBS for subscriptions
    });
    console.log('Available products:', products);
  } catch (error) {
    console.error('Error getting products:', error);
  }
};

Purchase a product

To initiate a purchase:

const purchaseProduct = async (productId: string) => {
  try {
    const transaction = await NativePurchases.purchaseProduct({
      productIdentifier: productId,
      productType: PURCHASE_TYPE.INAPP // or PURCHASE_TYPE.SUBS for subscriptions
    });
    console.log('Purchase successful:', transaction);
    // Handle the successful purchase (e.g., unlock content, update UI)
  } catch (error) {
    console.error('Purchase failed:', error);
  }
};

Restore purchases

To restore previously purchased products:

const restorePurchases = async () => {
  try {
    const { customerInfo } = await NativePurchases.restorePurchases();
    console.log('Restored purchases:', customerInfo);
    // Update your app's state based on the restored purchases
  } catch (error) {
    console.error('Failed to restore purchases:', error);
  }
};

Example: Implementing a simple store

Here's a basic example of how you might implement a simple store in your app:

import { Capacitor } from '@capacitor/core';
import { NativePurchases, PURCHASE_TYPE, Product } from '@capgo/native-purchases';

class Store {
  private products: Product[] = [];

  async initialize() {
    if (Capacitor.isNativePlatform()) {
      try {
        await this.checkBillingSupport();
        await this.loadProducts();
      } catch (error) {
        console.error('Store initialization failed:', error);
      }
    }
  }

  private async checkBillingSupport() {
    const { isBillingSupported } = await NativePurchases.isBillingSupported();
    if (!isBillingSupported) {
      throw new Error('Billing is not supported on this device');
    }
  }

  private async loadProducts() {
    const productIds = ['premium_subscription', 'remove_ads', 'coin_pack'];
    const { products } = await NativePurchases.getProducts({
      productIdentifiers: productIds,
      productType: PURCHASE_TYPE.INAPP
    });
    this.products = products;
  }

  getProducts() {
    return this.products;
  }

  async purchaseProduct(productId: string) {
    try {
      const transaction = await NativePurchases.purchaseProduct({
        productIdentifier: productId,
        productType: PURCHASE_TYPE.INAPP
      });
      console.log('Purchase successful:', transaction);
      // Handle the successful purchase
      return transaction;
    } catch (error) {
      console.error('Purchase failed:', error);
      throw error;
    }
  }

  async restorePurchases() {
    try {
      const { customerInfo } = await NativePurchases.restorePurchases();
      console.log('Restored purchases:', customerInfo);
      // Update app state based on restored purchases
      return customerInfo;
    } catch (error) {
      console.error('Failed to restore purchases:', error);
      throw error;
    }
  }
}

// Usage
const store = new Store();
await store.initialize();

// Display products
const products = store.getProducts();
console.log('Available products:', products);

// Purchase a product
try {
  await store.purchaseProduct('premium_subscription');
  console.log('Purchase completed successfully');
} catch (error) {
  console.error('Purchase failed:', error);
}

// Restore purchases
try {
  await store.restorePurchases();
  console.log('Purchases restored successfully');
} catch (error) {
  console.error('Failed to restore purchases:', error);
}

This example provides a basic structure for initializing the store, loading products, making purchases, and restoring previous purchases. You'll need to adapt this to fit your specific app's needs, handle UI updates, and implement proper error handling and user feedback.

Backend Validation

It's crucial to validate receipts on your server to ensure the integrity of purchases. Here's an example of how to implement backend validation using a Cloudflare Worker:

Cloudflare Worker Setup Create a new Cloudflare Worker and follow the instructions in folder (validator)[/validator/README.md]

Then in your app, modify the purchase function to validate the receipt on the server:

import { Capacitor } from '@capacitor/core';
import { NativePurchases, PURCHASE_TYPE, Product, Transaction } from '@capgo/native-purchases';
import axios from 'axios'; // Make sure to install axios: npm install axios

class Store {
  // ... (previous code remains the same)

  async purchaseProduct(productId: string) {
    try {
      const transaction = await NativePurchases.purchaseProduct({
        productIdentifier: productId,
        productType: PURCHASE_TYPE.INAPP
      });
      console.log('Purchase successful:', transaction);
      
      // Immediately grant access to the purchased content
      await this.grantAccess(productId);
      
      // Initiate server-side validation asynchronously
      this.validatePurchaseOnServer(transaction).catch(console.error);
      
      return transaction;
    } catch (error) {
      console.error('Purchase failed:', error);
      throw error;
    }
  }

  private async grantAccess(productId: string) {
    // Implement logic to grant immediate access to the purchased content
    console.log(`Granting access to ${productId}`);
    // Update local app state, unlock features, etc.
  }

  private async validatePurchaseOnServer(transaction: Transaction) {
    const serverUrl = 'https://your-server-url.com/validate-purchase';
    try {
      const response = await axios.post(serverUrl, {
        transactionId: transaction.transactionId,
        platform: Capacitor.getPlatform(),
        // Include any other relevant information
      });

      console.log('Server validation response:', response.data);
      // The server will handle the actual validation with the Cloudflare Worker
    } catch (error) {
      console.error('Error in server-side validation:', error);
      // Implement retry logic or notify the user if necessary
    }
  }
}

// Usage remains the same
const store = new Store();
await store.initialize();

try {
  await store.purchaseProduct('premium_subscription');
  console.log('Purchase completed successfully');
} catch (error) {
  console.error('Purchase failed:', error);
}

Now, let's look at how the server-side (Node.js) code might handle the validation:

import express from 'express';
import axios from 'axios';

const app = express();
app.use(express.json());

const CLOUDFLARE_WORKER_URL = 'https://your-cloudflare-worker-url.workers.dev';

app.post('/validate-purchase', async (req, res) => {
  const { transactionId, platform } = req.body;

  try {
    const endpoint = platform === 'ios' ? '/apple' : '/google';
    const validationResponse = await axios.post(`${CLOUDFLARE_WORKER_URL}${endpoint}`, {
      receipt: transactionId
    });

    const validationResult = validationResponse.data;

    // Process the validation result
    if (validationResult.isValid) {
      // Update user status in the database
      // await updateUserStatus(userId, 'paid');
      
      // Log the successful validation
      console.log(`Purchase validated for transaction ${transactionId}`);
      
      // You might want to store the validation result for future reference
      // await storeValidationResult(userId, transactionId, validationResult);
    } else {
      // Handle invalid purchase
      console.warn(`Invalid purchase detected for transaction ${transactionId}`);
      // You might want to flag this for further investigation
      // await flagSuspiciousPurchase(userId, transactionId);
    }

    // Always respond with a success to the app
    // This ensures the app doesn't block the user's access
    res.json({ success: true });
  } catch (error) {
    console.error('Error validating purchase:', error);
    // Still respond with success to the app
    res.json({ success: true });
    // You might want to log this error or retry the validation later
    // await logValidationError(userId, transactionId, error);
  }
});

// Start the server
app.listen(3000, () => console.log('Server running on port 3000'));

Key points about this approach:

  1. The app immediately grants access after a successful purchase, ensuring a smooth user experience.
  2. The app initiates server-side validation asynchronously, not blocking the user's access.
  3. The server handles the actual validation by calling the Cloudflare Worker.
  4. The server always responds with success to the app, even if validation fails or encounters an error.
  5. The server can update the user's status in the database, log results, and handle any discrepancies without affecting the user's immediate experience.

Comments on best practices:

// After successful validation:
// await updateUserStatus(userId, 'paid');

// It's crucial to not block or revoke access immediately if validation fails
// Instead, flag suspicious transactions for review:
// if (!validationResult.isValid) {
//   await flagSuspiciousPurchase(userId, transactionId);
// }

// Implement a system to periodically re-check flagged purchases
// This could be a separate process that runs daily/weekly

// Consider implementing a grace period for new purchases
// This allows for potential delays in server communication or store processing
// const GRACE_PERIOD_DAYS = 3;
// if (daysSincePurchase < GRACE_PERIOD_DAYS) {
//   grantAccess = true;
// }

// For subscriptions, regularly check their status with the stores
// This ensures you catch any cancelled or expired subscriptions
// setInterval(checkSubscriptionStatuses, 24 * 60 * 60 * 1000); // Daily check

// Implement proper error handling and retry logic for network failures
// This is especially important for the server-to-Cloudflare communication

// Consider caching validation results to reduce load on your server and the stores
// const cachedValidation = await getCachedValidation(transactionId);
// if (cachedValidation) return cachedValidation;

This approach balances immediate user gratification with proper server-side validation, adhering to Apple and Google's guidelines while still maintaining the integrity of your purchase system.

API

restorePurchases()

restorePurchases() => any

Restores a user's previous and links their appUserIDs to any user's also using those .

Returns: any


purchaseProduct(...)

purchaseProduct(options: { productIdentifier: string; planIdentifier?: string; productType?: PURCHASE_TYPE; quantity?: number; }) => any

Started purchase process for the given product.

| Param | Type | Description | | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------- | | options | { productIdentifier: string; planIdentifier?: string; productType?: PURCHASE_TYPE; quantity?: number; } | - The product to purchase |

Returns: any


getProducts(...)

getProducts(options: { productIdentifiers: string[]; productType?: PURCHASE_TYPE; }) => any

Gets the product info associated with a list of product identifiers.

| Param | Type | Description | | ------------- | -------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | | options | { productIdentifiers: {}; productType?: PURCHASE_TYPE; } | - The product identifiers you wish to retrieve information for |

Returns: any


getProduct(...)

getProduct(options: { productIdentifier: string; productType?: PURCHASE_TYPE; }) => any

Gets the product info for a single product identifier.

| Param | Type | Description | | ------------- | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- | | options | { productIdentifier: string; productType?: PURCHASE_TYPE; } | - The product identifier you wish to retrieve information for |

Returns: any


isBillingSupported()

isBillingSupported() => any

Check if billing is supported for the current device.

Returns: any


getPluginVersion()

getPluginVersion() => any

Get the native Capacitor plugin version

Returns: any


Interfaces

CustomerInfo

| Prop | Type | Description | | ------------------------------------ | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | activeSubscriptions | [string] | Set of active subscription skus | | allPurchasedProductIdentifiers | [string] | Set of purchased skus, active and inactive | | nonSubscriptionTransactions | {} | Returns all the non-subscription a user has made. The are ordered by purchase date in ascending order. | | latestExpirationDate | string | null | The latest expiration date of all purchased skus | | firstSeen | string | The date this user was first seen in RevenueCat. | | originalAppUserId | string | The original App User Id recorded for this user. | | requestDate | string | Date when this info was requested | | originalApplicationVersion | string | null | Returns the version number for the version of the application when the user bought the app. Use this for grandfathering users when migrating to subscriptions. This corresponds to the value of CFBundleVersion (in iOS) in the Info.plist file when the purchase was originally made. This is always null in Android | | originalPurchaseDate | string | null | Returns the purchase date for the version of the application when the user bought the app. Use this for grandfathering users when migrating to subscriptions. | | managementURL | string | null | URL to manage the active subscription of the user. If this user has an active iOS subscription, this will point to the App Store, if the user has an active Play Store subscription it will point there. If there are no active subscriptions it will be null. If there are multiple for different platforms, it will point to the device store. |

Transaction

| Prop | Type | Description | | ------------------- | ------------------- | -------------------------------------------- | | transactionId | string | RevenueCat Id associated to the transaction. |

Product

| Prop | Type | Description | | --------------------------------- | ----------------------------------------------------------------------- | ------------------------------------------------------------------------ | | identifier | string | Product Id. | | description | string | Description of the product. | | title | string | Title of the product. | | price | number | Price of the product in the local currency. | | priceString | string | Formatted price of the item, including its currency sign, such as €3.99. | | currencyCode | string | Currency code for price and original price. | | currencySymbol | string | Currency symbol for price and original price. | | isFamilyShareable | boolean | Boolean indicating if the product is sharable with family | | subscriptionGroupIdentifier | string | Group identifier for the product. | | subscriptionPeriod | SubscriptionPeriod | The Product subcription group identifier. | | introductoryPrice | SKProductDiscount | null | The Product introductory Price. | | discounts | {} | The Product discounts list. |

SubscriptionPeriod

| Prop | Type | Description | | ------------------- | ------------------- | --------------------------------------- | | numberOfUnits | number | The Subscription Period number of unit. | | unit | number | The Subscription Period unit. |

SKProductDiscount

| Prop | Type | Description | | ------------------------ | ----------------------------------------------------------------- | ------------------------------------------------------------------------ | | identifier | string | The Product discount identifier. | | type | number | The Product discount type. | | price | number | The Product discount price. | | priceString | string | Formatted price of the item, including its currency sign, such as €3.99. | | currencySymbol | string | The Product discount currency symbol. | | currencyCode | string | The Product discount currency code. | | paymentMode | number | The Product discount paymentMode. | | numberOfPeriods | number | The Product discount number Of Periods. | | subscriptionPeriod | SubscriptionPeriod | The Product discount subscription period. |

Enums

PURCHASE_TYPE

| Members | Value | Description | | ----------- | -------------------- | ---------------------------------- | | INAPP | "inapp" | A type of SKU for in-app products. | | SUBS | "subs" | A type of SKU for subscriptions. |