InAppify

Web and PWA SDK

Use @inappifylabs/web-sdk 0.2.0 for sessions, authentication, offerings, discounts, checkout/returns and Flutter Web bridging.

Web SDK is a headless library for websites/PWAs. It does not add checkout UI/paywall rendering, StoreKit, Bazaar or Myket billing to browsers. This guide targets 0.2.0, independently versioned from Flutter.

Installation and prerequisites

npm install --save-exact @inappifylabs/web-sdk@0.2.0

Enable the Web App, approved origin, offerings and checkout. Guest purchases require the corresponding feature. Attribute writes require allowlists. Use a Public Web App Key, not mobile/server keys. Initialize in the browser after browser APIs are ready; don't share one SSR customer session between users.

Create one client

import { InAppify } from '@inappifylabs/web-sdk';

export async function startWebSdk(publicWebKey: string) {
  return InAppify.configure({
    apiKey: publicWebKey,
    country: 'IR',
    appVersion: '2.0.0', // Host app version, not SDK version.
  });
}

Keep one client in application session scope. Configure creates/restores the session, loads customer state and prefetches offerings. Defaults: IR country, empty appVersion, 15-second timeout, five-minute offering TTL, prefetch enabled. appVersion is the host version; SDK version is embedded in its build.

Access tokens stay in memory; refresh tokens use available IndexedDB storage. Memory-only sessions don't promise restoration after reload. CustomerInfo has no separate persistent cache. Don't rewrite provider storage or copy tokens into localStorage.

Identity and offerings

Use client.logIn({identityTicket}) or client.logIn({challengeId, code}), then client.logOut() for logout. Tickets must be fresh from your authenticated backend; identity examples also cover OTP.

getCurrentOffering(), getCurrentOffering('upgrade'), and getOffering('special_offer') cover three selection modes. getOfferings() returns a collection with array all. Don't reconstruct packages from names. See offerings.

Purchase and return

import type { InAppify } from '@inappifylabs/web-sdk';

export async function createWebPayment(client: InAppify, savedAttemptId: string) {
  const offering = await client.getCurrentOffering('upgrade');
  const selected = offering?.packages[0];
  if (!selected)
    return null;
  return client.purchase({
    package: selected,
    idempotencyKey: savedAttemptId,
  }, { redirect: false });
}

export async function verifyWebReturn(client: InAppify, savedCheckoutId: string) {
  const result = await client.handlePaymentReturn(savedCheckoutId, {
    timeoutMs: 60000,
    intervalMs: 1500,
  });
  return result.checkout.status === 'Paid';
}

purchase({package}) redirects by default. The example disables it to return checkout data. Before navigating to nextAction.url, durably save checkout ID and attempt/identity context, and apply the host destination/origin policy. createCheckout(params) never navigates itself.

getCheckoutStatus(id) queries once; handlePaymentReturn(id, options) polls for at most 60 seconds by default, every 1500 ms. Only authoritative server Paid proves payment. Refresh CustomerInfo for access; consumables still need a durable grant. Query parameters, browser closure or poll cancellation don't confirm/cancel payment. Timeout leaves uncertainty unresolved.

CustomerInfo and sync

The following are alternatives; choose one policy in real code, rather than issuing every call:

import type { InAppify } from '@inappifylabs/web-sdk';
import { CacheFetchPolicy } from '@inappifylabs/web-sdk';

export async function customerReadAlternatives(client: InAppify) {
  await client.getCustomerInfo(); // FETCH_CURRENT by default.
  await client.getCustomerInfo(CacheFetchPolicy.FETCH_CURRENT);
  await client.getCustomerInfo(CacheFetchPolicy.CACHE_ONLY);
  await client.getCustomerInfo(CacheFetchPolicy.NOT_STALE_CACHED_OR_CURRENT);
  await client.getCustomerInfo(CacheFetchPolicy.CACHED_OR_FETCHED);
  return client.hasEntitlement('premium');
}

JS defaults to FETCH_CURRENT. Other modes provide cache-only, fresh-cache-or-server, and immediate cache with stale refresh. Staleness is five minutes visible/25 hours hidden. JS entitlements are arrays; use client access helpers instead of copying Flutter map access.

setAttributes({...}) writes immediately. syncAttributesAndOfferingsIfNeeded() refreshes customer state then offerings, with five admissions per rolling 60 seconds. Failures count and identity changes don't reset quota; excess calls return current-session cache or RATE_LIMITED_NO_CACHE. There is no native offline attribute queue.

Discounts and failures

validateDiscountCode(code) prices current; validateDiscountCode(code, offeringId) prices the explicit offer. For a placement, pass the selected offering ID. Never rewrite blank/invalid IDs to default. Discount examples use server-priced data.

Failed Promises throw/reject; identify structured failures with isInAppifyError(error). An exception/aborted signal does not prove a payment didn't commit. Legacy checkEntitlement/force options remain compatible; prefer CustomerInfo policies and synchronous access helpers for new code.

Flutter Web: bridge before bootstrap

The npm dependency must be bundled by the host build; Flutter doesn't turn bare npm imports into browser assets. Use the Flutter repository's web/inappify_web_sdk_entry.mjs as a bundler input. Its boundary resembles:

import { InAppify, isInAppifyError, SDK_VERSION } from '@inappifylabs/web-sdk';

Object.defineProperty(globalThis, '__inappifyFlutterWebSdk', {
  value: Object.freeze({ InAppify, version: SDK_VERSION, isInAppifyError }),
  writable: false,
  configurable: false,
});

Load the self-hosted bundle output, for example inappify-provider.js, and replace the duplicated Flutter bootstrap with this ordered loader:

<script type="module">
  await import('./inappify-provider.js');
  const bootstrap = document.createElement('script');
  bootstrap.src = './flutter_bootstrap.js';
  document.body.appendChild(bootstrap);
</script>

That file must be bundled output, not raw source with a bare import. The official entry guards duplicate installation; don't use the illustrative boundary to overwrite an existing one. Allow required assets/services in host CSP. Use only the Flutter facade afterward; don't create a parallel JS client.

Guest startup uses InappifySdk.configureResult(apiKey: 'YOUR_PUBLIC_WEB_APP_KEY'); authenticated startup uses the Web helper.

Flutter Web: checkout and gaps

import 'package:inappify_sdk/inappify_sdk.dart';

Future<InappifySdkResult<InappifyCheckoutResult>> createDiscountedCheckout({
  required PackageModel selected,
  required String code,
  required String savedAttemptId,
}) {
  final product = selected.product?.identifier;
  final offering = selected.offeringIdentifier;
  final package = selected.identifier;
  if (product == null || offering == null || package == null) {
    throw ArgumentError('Use a complete SDK catalog Package.');
  }
  return InappifySdk.createCheckout(
    productIdentifier: product,
    offeringIdentifier: offering,
    packageIdentifier: package,
    discountCode: code,
    idempotencyKey: savedAttemptId,
  );
}

Future<bool> verifyFlutterWebReturn(String savedCheckoutId) async {
  final result = await InappifySdk.handlePaymentReturnResult(
    checkoutId: savedCheckoutId, timeoutMs: 60000, intervalMs: 1500,
  );
  return result.isSuccess && result.data?.isPaid == true;
}

Identifiers come from the valid/discounted offering's Package. createCheckout carries an explicit discountCode, absent from Flutter's InappifyPurchaseParams. purchaseResult(Package) also works, but projects checkout into PurchaseResult and calls JS with redirects disabled; the host navigates.

Without checkoutId, handlePaymentReturnResult() uses the pending checkout in sessionStorage. getPendingCheckoutIdResult() and getCheckoutStatus(checkoutId:) are available. Storage is correlation, not proof of payment; storage failure doesn't establish that no checkout exists.

The Flutter 0.2.1 bridge doesn't implement combined sync/native delivery/history, and does not forward fetchPolicy. getOffering works through the collection helper. Follow the support matrix; don't bypass gaps through another client or private src classes.

Web 0.2.0 source · API reference