InAppify

Purchase a Package and track the outcome

Package-based store/Direct purchases, Kotlin Activity, attempt keys and restart-safe Android purchase outcomes.

Fetch the selected offering and retain the original Package. The SDK derives product/offering/package identifiers and product type; don't reconstruct those from display text.

Flutter

import 'package:inappify_sdk/inappify_sdk.dart';

Future<Map<String, Object?>> purchaseSelected(
  PackageModel selected, String savedAttemptId,
) async {
  final result = await InappifySdk.purchaseResult(InappifyPurchaseParams(
    packageToPurchase: selected,
    idempotencyKey: savedAttemptId,
  ));
  if (!result.isSuccess) return {}; // Keep uncertain attempt/context for recovery.
  final purchase = result.data;
  // Route to access refresh, durable delivery, or validated browser navigation.
  return {
    'workflow': purchase?.mobile?.storePurchaseStatus,
    'deliveryId': purchase?.mobile?.deliveryId,
    'paymentUrl': purchase?.url,
  };
}

packageToPurchase is required. idempotencyKey, paywallId, and paywallRevision default to null; attribution ID/revision must be a matching paywall pair. The SDK generates a key when omitted. For host restart-safe correlation, save a valid key before purchase and reuse it only for that logical attempt: 1–128 letters/digits or _ . : -.

Android supplies its Activity through the bridge. New purchase params have no API key, market/RSA, discount amount, isCrypto or product-type override. Convenience purchase(params) is nullable; prefer Result for financial decisions.

Kotlin: store and Direct

import android.app.Activity
import com.inappify.sdk.*

suspend fun purchaseStore(
    client: InappifyV2Client,
    activity: Activity,
    selected: InappifyPackage,
    savedAttemptId: String,
) = client.purchase(
    InappifyPurchaseParams.Builder(activity, selected)
        .idempotencyKey(savedAttemptId)
        .build()
)

suspend fun purchaseDirect(
    client: InappifyV2Client,
    selected: InappifyPackage,
    savedAttemptId: String,
) = client.purchase(
    InappifyPurchaseParams.Builder(selected)
        .idempotencyKey(savedAttemptId)
        .build()
)

A new store purchase requires the Activity; the Activity-free Builder serves Direct or existing paid work. Do not retain Activity-backed params past its lifecycle. Optional paywallAttribution(id, revision) is valid Direct presentation context, not a price setter.

Operation success versus payment completion

StateHost action
NEEDTOPAY with URLContinue in a trusted browser; no access grant
PROCESSING or uncertain retryable failureWait/reconcile the same attempt; no second payment
COMPLETED, RESTORED, ALREADY_PROCESSEDRead CustomerInfo for access
DELIVERY_REQUIREDDurably grant the consumable once
CONSUME_REQUIREDComplete consumption without re-crediting
REJECTEDNo access/inventory grant

Not every enum state arrives as Success; some pending states are recoverable Failures. Flutter exposes mobile.storePurchaseStatus, Kotlin storePurchaseStatus. isSuccess describes the SDK operation only.

Direct payments

The host opens the returned URL. On mobile await InappifySdk.isPaymentUrlAllowed(url) and require success/data true; Kotlin's extension client.isPaymentUrlAllowed(url) returns Boolean. Follow the SDK destination policy; never substitute arbitrary query/user URLs.

New Android Direct results retain the original attempt through compatible backend support. On iOS this release's key only correlates local result/failure; it is not sent in the Direct request. Replaying it does not promise server deduplication. Reconcile uncertainty before another payment.

Web has a separate checkout contract and verifies returns using checkout ID; follow the Web example. A return link, browser closure or successful checkout creation is not proof of payment.

Android: track across process restart

import 'dart:async';
import 'package:inappify_sdk/inappify_sdk.dart';

StreamSubscription<InappifyPurchaseAttempt> observeAndroidPurchases(
  void Function(InappifyPurchaseAttempt) reconcileMatchingAttempt,
) => InappifySdk.purchaseUpdates.listen(reconcileMatchingAttempt);

Future<void> inspectSavedAttempt(String savedAttemptId) async {
  final result = await InappifySdk.getPurchaseAttempt(savedAttemptId);
  if (!result.isSuccess || result.data == null) return; // Retain unknown attempt.
  final attempt = result.data!;
  if (attempt.attemptId != savedAttemptId) return;
  // Also match the saved customer/app/product/offering/market context.
  // Inspect attempt.status, attempt.purchase and attempt.failure.
}
import com.inappify.sdk.*

suspend fun inspectSavedAttempt(client: InappifyV2Client, savedAttemptId: String) {
    when (val result = client.getPurchaseAttempt(savedAttemptId)) {
        is InappifyResult.Failure -> return
        is InappifyResult.Success -> {
            val attempt = result.data ?: return // Unknown: retain host journal.
            if (attempt.attemptId != savedAttemptId) return
            // Match original scope; inspect status, purchase and error.
        }
    }
}

Subscribe before purchase and cancel when the UI owner ends. Kotlin client.addEventListener { event -> event.purchaseAttempt ... } returns a closable registration. getPurchaseAttempts() also reads current-scope history without HTTP/store calls.

Attempt statuses are pending/succeeded/failed. Successful null lookup means unknown, not failed. Android history is encrypted, bounded and app/customer/market-scoped. Events can repeat or be missed while closed; query the saved attempt after configure. Recorded success is not current CustomerInfo or durable inventory authority.

Network failure, account change, empty sync results or unrelated events must not erase the host attempt or start a new payment. Attempt-history APIs are unsupported on iOS and Flutter Web.

Consumables · Discounts · Access and recovery