InAppify

Deliver consumable purchases

Fulfill once through Android handlers or manual sync/confirm, with durable inventory commits and iOS/Web differences.

The SDK owns verification and delivery workflow, not your inventory. DELIVERY_REQUIRED means fulfillment is pending, not that coins/credits were granted.

Durable grant contract

Commit inventory and the unique delivery key in one host database transaction. Scope it by app, original customer, source and delivery ID. Replaying a granted delivery must succeed without another credit. Memory flags or separate preferences writes don't establish this guarantee. Your trusted catalog determines credit quantity; the SDK does not infer it.

grantOnce below is a host-owned callback implementing that transaction. Validate the owner inside the transaction and quiesce/coordinate async delivery during account changes.

Android: registered handler

Register before configure:

import 'package:inappify_sdk/inappify_sdk.dart';

Future<InappifySdkResult<void>> registerAndroidDelivery(
  Future<void> Function(InappifyConsumableDelivery) grantOnce,
) => InappifySdk.setConsumableDeliveryHandler((delivery) async {
  try {
    await grantOnce(delivery); // Durable, scoped, idempotent host transaction.
    return InappifyDeliveryResult.delivered;
  } catch (_) {
    return InappifyDeliveryResult.retryLater;
  }
});
import com.inappify.sdk.*

fun registerDelivery(
    client: InappifyV2Client,
    grantOnce: suspend (InappifyConsumableDelivery) -> Unit,
): InappifyListenerRegistration = client.setConsumableDeliveryHandler { delivery ->
    try {
        grantOnce(delivery) // Commit durable inventory before returning.
        InappifyDeliveryResult.DELIVERED
    } catch (cancelled: kotlinx.coroutines.CancellationException) {
        throw cancelled
    } catch (_: Exception) {
        InappifyDeliveryResult.RETRY_LATER
    }
}

Return delivered/DELIVERED only after commit; the SDK acknowledges and continues consumption/reporting. Never call confirmDelivery inside the handler. Failure, missing handler or retryLater leaves work pending. Kotlin returns a closable registration; Flutter removes a handler with null.

Manual iOS Direct or Android without a handler

import 'package:inappify_sdk/inappify_sdk.dart';

Future<void> fulfillManually({
  required Future<void> Function(InappifyConsumableDelivery) grantOnce,
  required bool Function() stillSameCustomer,
}) async {
  final result = await InappifySdk.syncPendingConsumables();
  if (!result.isSuccess || !stillSameCustomer()) return;
  for (final delivery in result.data?.pendingDeliveries ??
      <InappifyConsumableDelivery>[]) {
    if (!stillSameCustomer()) return;
    await grantOnce(delivery);
    if (!stillSameCustomer()) return;
    final confirmed = await InappifySdk.confirmDelivery(delivery.deliveryId);
    if (!confirmed.isSuccess) return; // Keep the committed grant; never re-credit.
  }
}

Use only without a competing handler. stillSameCustomer complements, not replaces, transactional inventory ownership checks. Kotlin follows the same pattern with client.syncPendingConsumables() and client.confirmDelivery(deliveryId); its ID is Long.

A failed confirm must not erase or repeat a committed grant. The SDK retains checkpoints. Native consumption applies only to consumables, never subscriptions/non-consumables.

Direct JavaScript

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

export function registerDelivery(
  client: InAppify,
  grantOnce: (delivery: ConsumableDelivery) => Promise<void>,
) {
  return client.setConsumableDeliveryHandler(async (delivery) => {
    try {
      await grantOnce(delivery);
      return DeliveryResult.DELIVERED;
    } catch {
      return DeliveryResult.RETRY_LATER;
    }
  }); // Returns an unregister function.
}

export async function synchronizeDeliveries(client: InAppify) {
  return client.syncPendingConsumables();
}

The registered handler must explicitly return DeliveryResult.DELIVERED. Without a handler, syncPendingConsumables() returns summary/pendingDeliveries; durably grant before client.confirmDelivery(id). Registration returns an unregister function. The legacy inline-handler overload remains compatible; choose the explicit form for new code.

Platform limits

iOS has manual Direct fulfillment, not native handler registration. Flutter Web 0.2.1 doesn't provide native handler/sync/confirm. Direct JS support does not establish Flutter facade support. Don't bypass it with a second JS client. Consult the platform matrix.

Essential tests

Test duplicate delivery, termination after commit/before confirm, failed confirmation, restart and account change. Each delivery grants inventory at most once; confirmation never precedes the durable commit.

Access and recovery · Troubleshooting