diff --git a/code_blocks/_projects/web-billing/web-billing-doc-snippets.ts b/code_blocks/_projects/web-billing/web-billing-doc-snippets.ts index 8506e849..5672ac9b 100644 --- a/code_blocks/_projects/web-billing/web-billing-doc-snippets.ts +++ b/code_blocks/_projects/web-billing/web-billing-doc-snippets.ts @@ -1,206 +1,248 @@ -import type { CustomerInfo, Package, Product } from "@revenuecat/purchases-js"; -import { ErrorCode, Purchases, PurchasesError } from "@revenuecat/purchases-js"; +import type {CustomerInfo, Package, Product} from "@revenuecat/purchases-js"; +import {ErrorCode, Purchases, PurchasesError} from "@revenuecat/purchases-js"; -const authentication = { getAppUserId: () => "test" }; +const authentication = {getAppUserId: () => "test"}; function configuringSDK(WEB_BILLING_PUBLIC_API_KEY: string) { - // MARK: Configuring SDK - const appUserId = authentication.getAppUserId(); // Replace with your own authentication system - const purchases = Purchases.configure(WEB_BILLING_PUBLIC_API_KEY, appUserId); - // END - return purchases; + // MARK: Configuring SDK + const appUserId = authentication.getAppUserId(); // Replace with your own authentication system + const purchases = Purchases.configure(WEB_BILLING_PUBLIC_API_KEY, appUserId); + // END + return purchases; +} + +function configuringSDKOutOutOfAutomaticUTMCollection(WEB_BILLING_PUBLIC_API_KEY: string) { + // MARK: Configuring SDK without automatic UTM collection + const appUserId = authentication.getAppUserId(); // Replace with your own authentication system + const httpConfig = {}; // Any setting you want to customize in purchases-js HTTP client. + const flagsConfig = {autoCollectUTMAsMetadata: false}; + const purchases = Purchases.configure(WEB_BILLING_PUBLIC_API_KEY, appUserId, httpConfig, flagsConfig); + // END + return purchases; } function configuringSDKWithAnonUser(WEB_BILLING_PUBLIC_API_KEY: string) { - // MARK: Configuring SDK Anonymous - // This function will generate a unique anonymous ID for the user. - // Make sure to enable the Redemption Links feature in the RevenueCat dashboard and use the - // redemption link to redeem the purchase in your mobile app. - const appUserId = Purchases.generateRevenueCatAnonymousAppUserId(); - const purchases = Purchases.configure(WEB_BILLING_PUBLIC_API_KEY, appUserId); - // END - return purchases; + // MARK: Configuring SDK Anonymous + // This function will generate a unique anonymous ID for the user. + // Make sure to enable the Redemption Links feature in the RevenueCat dashboard and use the + // redemption link to redeem the purchase in your mobile app. + const appUserId = Purchases.generateRevenueCatAnonymousAppUserId(); + const purchases = Purchases.configure(WEB_BILLING_PUBLIC_API_KEY, appUserId); + // END + return purchases; } async function getCustomerInfo(): Promise { - let customerInfo: CustomerInfo | null = null; - // MARK: Getting customer information - try { - customerInfo = await Purchases.getSharedInstance().getCustomerInfo(); - // access latest customerInfo - } catch (e) { - // Handle errors fetching customer info - } - // END - return customerInfo; + let customerInfo: CustomerInfo | null = null; + // MARK: Getting customer information + try { + customerInfo = await Purchases.getSharedInstance().getCustomerInfo(); + // access latest customerInfo + } catch (e) { + // Handle errors fetching customer info + } + // END + return customerInfo; } async function checkForSpecificEntitlement(grantEntitlementAccess: () => void) { - const customerInfo = await Purchases.getSharedInstance().getCustomerInfo(); - if (!customerInfo) return; - // MARK: Check for specific entitlement - if ("gold_entitlement" in customerInfo.entitlements.active) { - // Grant user access to the entitlement "gold_entitlement" - grantEntitlementAccess(); - } - // END + const customerInfo = await Purchases.getSharedInstance().getCustomerInfo(); + if (!customerInfo) return; + // MARK: Check for specific entitlement + if ("gold_entitlement" in customerInfo.entitlements.active) { + // Grant user access to the entitlement "gold_entitlement" + grantEntitlementAccess(); + } + // END } async function checkForAnyEntitlement(grantEntitlementAccess: () => void) { - const customerInfo = await Purchases.getSharedInstance().getCustomerInfo(); - if (!customerInfo) return; - // MARK: Check for any entitlement - if (Object.keys(customerInfo.entitlements.active).length > 0) { - // User has access to some entitlement, grant entitlement access - grantEntitlementAccess(); - } - // END + const customerInfo = await Purchases.getSharedInstance().getCustomerInfo(); + if (!customerInfo) return; + // MARK: Check for any entitlement + if (Object.keys(customerInfo.entitlements.active).length > 0) { + // User has access to some entitlement, grant entitlement access + grantEntitlementAccess(); + } + // END } async function getCurrentOffering(displayPackages: (pkg: Package[]) => void) { - // MARK: Get current offerings - try { - const offerings = await Purchases.getSharedInstance().getOfferings(); - if ( - offerings.current !== null && - offerings.current.availablePackages.length !== 0 - ) { - // Display packages for sale - displayPackages(offerings.current.availablePackages); + // MARK: Get current offerings + try { + const offerings = await Purchases.getSharedInstance().getOfferings(); + if ( + offerings.current !== null && + offerings.current.availablePackages.length !== 0 + ) { + // Display packages for sale + displayPackages(offerings.current.availablePackages); + } + } catch (e) { + // Handle errors } - } catch (e) { - // Handle errors - } - // END + // END } async function getOfferingForEUR(displayPackages: (pkg: Package[]) => void) { - // MARK: Get current offerings for EUR - try { - // Specify the currency to get offerings for - const offerings = await Purchases.getSharedInstance().getOfferings({ - currency: "EUR", - }); - if ( - offerings.current !== null && - offerings.current.availablePackages.length !== 0 - ) { - // Display packages for sale - displayPackages(offerings.current.availablePackages); + // MARK: Get current offerings for EUR + try { + // Specify the currency to get offerings for + const offerings = await Purchases.getSharedInstance().getOfferings({ + currency: "EUR", + }); + if ( + offerings.current !== null && + offerings.current.availablePackages.length !== 0 + ) { + // Display packages for sale + displayPackages(offerings.current.availablePackages); + } + } catch (e) { + // Handle errors } - } catch (e) { - // Handle errors - } - // END + // END } async function getCustomOffering(displayPackages: (pkg: Package[]) => void) { - // MARK: Get custom offering - try { - const offerings = await Purchases.getSharedInstance().getOfferings(); - if (offerings.all["experiment_group"].availablePackages.length !== 0) { - // Display packages for sale - displayPackages(offerings.all["experiment_group"].availablePackages); + // MARK: Get custom offering + try { + const offerings = await Purchases.getSharedInstance().getOfferings(); + if (offerings.all["experiment_group"].availablePackages.length !== 0) { + // Display packages for sale + displayPackages(offerings.all["experiment_group"].availablePackages); + } + } catch (e) { + // Handle errors } - } catch (e) { - // Handle errors - } - // END + // END } async function displayingPackages( - callback: (props: { - allPackages: Package[]; - monthlyPackage: Package | null; - customPackage: Package | null; - }) => void + callback: (props: { + allPackages: Package[]; + monthlyPackage: Package | null; + customPackage: Package | null; + }) => void ) { - const offerings = await Purchases.getSharedInstance().getOfferings(); - // MARK: Displaying packages - const allPackages = offerings.all["experiment_group"].availablePackages; - // -- - const monthlyPackage = offerings.all["experiment_group"].monthly; - // -- - const customPackage = - offerings.all["experiment_group"].packagesById[""]; - // END - callback({ allPackages, monthlyPackage, customPackage }); + const offerings = await Purchases.getSharedInstance().getOfferings(); + // MARK: Displaying packages + const allPackages = offerings.all["experiment_group"].availablePackages; + // -- + const monthlyPackage = offerings.all["experiment_group"].monthly; + // -- + const customPackage = + offerings.all["experiment_group"].packagesById[""]; + // END + callback({allPackages, monthlyPackage, customPackage}); } async function gettingProduct(displayProduct: (product: Product) => void) { - // MARK: Getting product - // Accessing / displaying the monthly product - try { - const offerings = await Purchases.getSharedInstance().getOfferings({ - currency: "USD", - }); - if (offerings.current && offerings.current.monthly) { - const product = offerings.current.monthly.webBillingProduct; - // Display the price and currency of the Web Billing Product - displayProduct(product); + // MARK: Getting product + // Accessing / displaying the monthly product + try { + const offerings = await Purchases.getSharedInstance().getOfferings({ + currency: "USD", + }); + if (offerings.current && offerings.current.monthly) { + const product = offerings.current.monthly.webBillingProduct; + // Display the price and currency of the Web Billing Product + displayProduct(product); + } + } catch (e) { + // Handle errors } - } catch (e) { - // Handle errors - } - // END + // END } async function purchasingPackage() { - // This can't really be tested currently - const pkg = (await Purchases.getSharedInstance().getOfferings()).current - ?.availablePackages[0]; - if (!pkg) return; - - // MARK: Purchasing package - try { - const { customerInfo } = await Purchases.getSharedInstance().purchase({ - rcPackage: pkg, - }); - if (Object.keys(customerInfo.entitlements.active).includes("pro")) { - // Unlock that great "pro" content + // This can't really be tested currently + const pkg = (await Purchases.getSharedInstance().getOfferings()).current + ?.availablePackages[0]; + if (!pkg) return; + + // MARK: Purchasing package + try { + const {customerInfo} = await Purchases.getSharedInstance().purchase({ + rcPackage: pkg, + }); + if (Object.keys(customerInfo.entitlements.active).includes("pro")) { + // Unlock that great "pro" content + } + } catch (e) { + if ( + e instanceof PurchasesError && + e.errorCode == ErrorCode.UserCancelledError + ) { + // User cancelled the purchase process, don't do anything + } else { + // Handle errors + } } - } catch (e) { - if ( - e instanceof PurchasesError && - e.errorCode == ErrorCode.UserCancelledError - ) { - // User cancelled the purchase process, don't do anything - } else { - // Handle errors + // END +} + +async function metadataWhilePurchasing() { + // This can't really be tested currently + const pkg = (await Purchases.getSharedInstance().getOfferings()).current + ?.availablePackages[0]; + if (!pkg) return; + + // MARK: Purchasing package with metadata + try { + const {customerInfo} = await Purchases.getSharedInstance().purchase({ + rcPackage: pkg, + metadata: { + a_custom_key: "a custom value", + }, + }); + if (Object.keys(customerInfo.entitlements.active).includes("pro")) { + // Unlock that great "pro" content + } + } catch (e) { + if ( + e instanceof PurchasesError && + e.errorCode == ErrorCode.UserCancelledError + ) { + // User cancelled the purchase process, don't do anything + } else { + // Handle errors + } } - } - // END + // END } async function configuringLocale() { - const pkg = (await Purchases.getSharedInstance().getOfferings()).current - ?.availablePackages[0]; - if (!pkg) return; - - // MARK: Configuring locale - try { - const { customerInfo, redemptionInfo } = - await Purchases.getSharedInstance().purchase({ - rcPackage: pkg, - selectedLocale: "es", - }); - } catch (e) { - // Something went wrong while purchasing - } - // END + const pkg = (await Purchases.getSharedInstance().getOfferings()).current + ?.availablePackages[0]; + if (!pkg) return; + + // MARK: Configuring locale + try { + const {customerInfo, redemptionInfo} = + await Purchases.getSharedInstance().purchase({ + rcPackage: pkg, + selectedLocale: "es", + }); + } catch (e) { + // Something went wrong while purchasing + } + // END } export { - configuringSDK, - getCustomerInfo, - checkForSpecificEntitlement, - checkForAnyEntitlement, - getCurrentOffering, - getOfferingForEUR, - getCustomOffering, - displayingPackages, - gettingProduct, - purchasingPackage, - configuringLocale, - configuringSDKWithAnonUser, + configuringSDK, + configuringSDKOutOutOfAutomaticUTMCollection, + getCustomerInfo, + checkForSpecificEntitlement, + checkForAnyEntitlement, + getCurrentOffering, + getOfferingForEUR, + getCustomOffering, + displayingPackages, + gettingProduct, + purchasingPackage, + configuringLocale, + configuringSDKWithAnonUser, + metadataWhilePurchasing }; diff --git a/docs/web/web-billing/custom-metadata.mdx b/docs/web/web-billing/custom-metadata.mdx new file mode 100644 index 00000000..01917ac8 --- /dev/null +++ b/docs/web/web-billing/custom-metadata.mdx @@ -0,0 +1,51 @@ +--- +title: Custom Metadata +slug: custom-metadata +hidden: false +--- + +Web Billing supports custom metadata that you can send along when executing the purchase. +The metadata will be propagated to webhook events and the Stripe customer object to enable its use in other systems, such as marketing or attribution tools. + +`purchases-js` will also automatically collect any `utm` parameters in the URL and send it as metadata when invoking `purchase()`. +You can opt-out of this behavior by setting the `autoCollectUTMAsMetadata` flag to `false` in the `configure` method. + +## How to send custom metadata when invoking purchase() + +When invoking `purchase()`, you can provide a `metadata` object using the `PurchaseParams` interface + +import webBillingContent from "!!raw-loader!@site/code_blocks/_projects/web-billing/web-billing-doc-snippets.ts"; + + + +The `metadata` property accepts a key-value object with any custom data you want to send along with the purchase. +The accepted values are `string`, `number`, `boolean` and `null`. + +After purchasing with the metadata set up you will receive it in the Initial Purchase webhook event and in the Stripe Customer object. + +![](/images/web-billing/metadata-in-initial-purchase-event.png) + +## Opt out of the automatic collection of UTM parameters + +You can opt-out of the automatic collection of UTM parameters by setting the `autoCollectUTMAsMetadata` flag to `false` in the `configure` method. +Here's an example: + + diff --git a/sidebars.js b/sidebars.js index 55909de9..3e338c6d 100644 --- a/sidebars.js +++ b/sidebars.js @@ -192,6 +192,7 @@ const webSDKCategory = Category({ Page({ slug: "multi-currency-support" }), Page({ slug: "localization" }), Page({ slug: "redemption-links" }), + Page({ slug: "custom-metadata" }), ], }), Page({ slug: "web-billing/managing-customer-subscriptions" }), diff --git a/static/images/web-billing/metadata-in-initial-purchase-event.png b/static/images/web-billing/metadata-in-initial-purchase-event.png new file mode 100644 index 00000000..f1b30d8a Binary files /dev/null and b/static/images/web-billing/metadata-in-initial-purchase-event.png differ