Skip to main content

kitty_sdk 0.13.5 copy "kitty_sdk: ^0.13.5" to clipboard
kitty_sdk: ^0.13.5 copied to clipboard

unlisted

Kitty Flutter SDK — device fingerprint attribution (Flare) plus authentication, user management, licenses and subscriptions (Account) for the Kitty Launch platform.

Kitty Flutter SDK #

Unified Flutter SDK for the Kitty Launch platform — combines Flare (device fingerprint attribution) and Account (authentication, user management, licenses, subscriptions) in a single package.

Install #

dependencies:
  kitty_sdk: ^0.13.5

Requires Flutter >=3.38.0 and Dart >=3.10.0. Coming from 0.12.x? Read Upgrading to 0.13.0 below first.

Upgrading to 0.13.3 #

No new minimums and no new setup. What a host app can notice:

  • With the Kitty backend's App Store attribution update (this reaches 0.13.2 too, once that backend update is deployed), a signed-out device is no longer logged in to an account that has a real email. If the purchase already belongs to such an account, the backend answers 409 (appstore_purchase_claimed). The SDK finishes the transaction without signing in, and onAppStoreObserverError receives ApiException(statusCode: 409) once per launch, and again after each paywall Restore, until the user signs in to that account the usual way; the SDK then claims the purchase for it. Treat this 409 as expected. An explicit KittyAccount.loginWithAppStore call throws the same exception. The paywall's Restore reports success whenever StoreKit finds the purchase, without signing the user in, so ask a signed-out user to sign in after a restore.
  • An iOS purchase made while a login is still loading its account, or after a login whose account fetch failed, is now claimed for that login, and only onAppStoreAutoClaim fires. Before, the SDK logged in with the purchase's App Store account: onAppStoreAutoLogin fired, and that virtual account replaced the new session.
  • An automatic App Store login that a login, a logout, a legacy adoption or a lost stored session overtakes — before or after it has stored its own credentials — no longer replaces the newer session and no longer finishes its purchase, and onAppStoreAutoLogin does not fire for it. The SDK claims the purchase under the newer session at once, or StoreKit redelivers it.
  • New KittyAccount.appAccountToken for apps with their own StoreKit code; see "Account — App Store attribution for your own StoreKit code".
  • New analytics event AppStoreAutoLoginSuperseded (replay). A superseded automatic login emits no LoginFailed, AppStoreAutoLoginFailed or AppStoreAutoLoginNoAccount, and onAppStoreObserverError receives a SessionSupersededException when its purchase is held instead of replayed.

Upgrading to 0.13.0 #

Use 0.13.2 or later rather than 0.13.0. (0.13.1 was not published to pub.dev; 0.13.2 contains its fixes.) It fixes two purchase defects present in 0.13.0: KittyBuyNow purchases now work on Android (broken since 0.10.0), and the paywall's pre-purchase cleanup no longer finishes a renewal whose own claim is still held. Everything below applies to 0.13.0 and later.

New minimums: Flutter >=3.38.0, Dart >=3.10.0 and in_app_purchase_storekit ^0.4.10. The SDK depends on the plugin directly, so if your app pins it, allow 0.4.10 or later.

On iOS the SDK finishes App Store transactions, and only after the backend confirms them. While the App Store observer runs (enableAppStoreObserver: true, the default), the SDK finishes iOS transactions, not your app:

  • A purchase is finished once the Kitty backend confirms it. Until then it stays unfinished, and StoreKit redelivers it on the next launch, where the SDK tries again. A purchase can therefore stay held across launches. Failures that may be temporary (offline, timeouts, 5xx, 429) just keep it held. A purchase the backend rejects or does not confirm is finished with an error on the fifth such launch.
  • As before, a signed-out buyer is logged in automatically from the purchase. The purchase then goes through the same confirmation. It is finished without confirmation only when that login is refused outright (404, or another 4xx except 429). Since the Kitty backend's App Store attribution update, that includes a 409 for a purchase that already belongs to an account with a real email (see Upgrading to 0.13.3).
  • If your app also listens to InAppPurchase.instance.purchaseStream, do not call completePurchase for iOS App Store purchases while the observer runs. Finishing early discards the receipt before the backend has recorded it.
  • Android purchase handling is unchanged: the paywalls still finish Google Play purchases immediately. KittyBuyNow purchases (InAppBuyNowBloc and the web paywall's BuyNowMessageHandler) now work on Android in 0.13.1. From 0.10.0 through 0.13.0 they failed with PlatformException(channel-error) before reaching Google Play.

Other changes a host app can notice:

  • onAppStoreAutoClaim now also fires after an anonymous auto-login, because the SDK claims the purchase right after logging in.
  • onAppStoreObserverError now also receives a StateError when a purchase is given up. It also receives a TimeoutException when a StoreKit finish (10 s), an auto-login or a claim (30 s) times out. Timed-out work is retried on the next launch.
  • New analytics event AppStoreClaimGivenUp (productId, attempts). New SharedPreferences key kitty_purchase_claim_attempts (a per-transaction retry counter).
  • ReauthRequiredException now means the session is really gone, and it has a new optional originStatusCode. A temporary refresh failure now surfaces as ApiException, TimeoutException, a transport error, or a parse error (FormatException / TypeError). Do not treat those as a logout. When a logout races a request, it surfaces with isCredentialsAbsent == true and no originStatusCode.
  • SessionSupersededException can reach calls made through KittyAccount.authorizedHttpClient or KittySdk.tokens. This happens in a rare race where a login overtakes a token refresh. Catch it by type (on SessionSupersededException) and retry.
  • When the user id is a UUID, signed-in iOS purchases now carry it as appAccountToken (StoreKit 2) or applicationUserName (StoreKit 1). Since 0.13.3, apps with their own StoreKit code read the same value from KittySdk.account.appAccountToken.
  • A login that replaces a signed-in user now drops the previous account at once and emits null on accountStream before the new account. If the new account then cannot be fetched (a 5xx or network error), the login throws and the app reads as signed out (isAuthenticated false, currentAccount null), but the new credentials are kept: refreshAccountInfo(), or the next launch, completes the session. Until then, purchases are handled as signed out up to 0.13.2; since 0.13.3 they are claimed for the new login.

Known limitations:

  • While a purchase is held, buying the same product again hits StoreKit's duplicate-purchase guard, and the SDK reports "purchase pending". The built-in flows show no message: InAppBuyNowBloc returns to the product list as it does on a cancel, and BuyNowMessageHandler closes the web paywall.
  • Only a running observer confirms transactions. In three setups the paywall's pre-purchase cleanup no longer finishes leftover transactions: enableAppStoreObserver: false, KittyBuyNow without KittyAccount, and macOS. There, a leftover transaction for the product being bought ends in the same silent "pending".
  • After a login, the SDK at once re-claims subscriptions and non-consumables bought before it. A held consumable, such as a coin pack, is claimed when StoreKit redelivers it on the next launch. Since 0.13.3, one case is not held: a purchase whose automatic App Store login a login or a legacy adoption overtook is claimed for that session at once, a single time, consumables included.
  • Confirmation needs StoreKit 2, which is the plugin's default. If your app opts into StoreKit 1, signed-in purchases cannot be confirmed. They are finished with an error on the fifth launch.

Quick Start #

import 'package:kitty_sdk/kitty_sdk.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  await KittySdk.initialize(
    config: KittySdkConfig(
      projectId: 'proj_xxx',
      accountBaseUrl: '<your-account-api-url>',
      flareBaseUrl: '<your-flare-api-url>',
    ),
  );

  runApp(const MyApp());
}

Usage #

Flare — fingerprint identification #

final match = await KittySdk.flare.identify();
if (match != null) {
  print('Matched! uid=${match.uid} type=${match.matchType}');
}

Account — authentication #

final account = await KittySdk.account.loginWithPassword(
  email: 'user@example.com',
  password: 'secret',
);

KittySdk.account.accountStream.listen((account) {
  // React to auth state changes
});

For an app that keeps a separate legacy login, check project membership before choosing the login flow:

final useKitty = await KittySdk.account.isUserInProject(
  email: email,
);
if (useKitty) {
  // Show the Kitty login flow.
} else {
  // Show the legacy login flow.
}

This checks for a Kitty project relation or license, including an expired one. It does not authenticate the email. Handle lookup errors separately; an API failure must not be treated as false. For Apple sign-in, perform provider routing after the Apple identity has been verified by the app's backend.

Account — App Store attribution for your own StoreKit code #

The SDK's paywalls attach the signed-in user's id to iOS purchases. If your app starts App Store purchases with its own StoreKit code, read the same value right before each purchase and pass it on:

final token = KittySdk.account.appAccountToken; // null: send nothing

// StoreKit 2 (in_app_purchase_storekit)
await SK2Product.purchase(
  productId,
  options: token == null ? null : SK2ProductPurchaseOptions(appAccountToken: token),
);

// StoreKit 1 (in_app_purchase)
await InAppPurchase.instance.buyNonConsumable(
  purchaseParam: PurchaseParam(productDetails: product, applicationUserName: token),
);

It is the id of the signed-in, loaded account when that id is a UUID. It is null while signed out, while a login is still loading its account, and for a non-UUID id; then send no token. While the App Store observer runs, the SDK still claims such a purchase for the signed-in user. Like every KittySdk.account member, it throws before KittySdk.initialize.

Account — licenses #

final user = KittySdk.account.currentAccount;
final license = user?.bestLicense;
if (license != null && license.isActive) {
  // User has active license
}

Account — feature access (entitlements) #

Gate features on the entitlement keys the billing plan declares (premium_stories, hd_export, …) rather than on plan names. The account-level list is the server's answer — the SDK reports it as given and never re-derives it from the licenses.

App Store review accounts are an exception: when the loaded account's effective email is test@<domain>, hasEntitlement(key) returns true for every key, regardless of licenses, expiration, or the server's entitlement list. Email matching is case-insensitive and ignores surrounding whitespace. The effective email is primaryEmail, falling back to the first entry in emails; a secondary test email does not override an ordinary primary email. This applies to the entitlement helper only: the raw entitlements list and license state stay as returned by the server.

The gate is tri-state:

  • null — no account is loaded: a fresh install before the first login, or after logout(). Fail closed and keep the feature locked until you know.
  • true / false — the most recently loaded account. Startup awaits a refresh when a session is restored, so normally this is the server's answer. If that startup refresh fails — an offline launch — the SDK keeps the persisted cache, so a false may be a stale cached answer rather than the server's current one.

The SDK does not retry a failed startup refresh on its own. Call refreshAccountInfo() yourself — on app resume, for instance — if the gate has to recover from an offline launch. A cache written by an SDK version before 0.12.0 rehydrates with no entitlements, so a user who does hold a key reads false until such a refresh lands. That is deliberate: it fails closed.

switch (KittySdk.account.hasEntitlement('premium_stories')) {
  case true:
    showPremiumStories();
  case false:
    showPaywall();
  case null:
    showLoading(); // not known yet — never treat this as granted
}

// All keys the account holds, or null when no account is loaded.
final keys = KittySdk.account.entitlements;

Rebuild on accountStream rather than reading this once at startup — that is how the UI picks up a later refresh, whether it comes from a login, a purchase, or your own refreshAccountInfo() call.

Account UI — theming #

Set the base SDK UI theme once:

await KittySdk.initialize(
  config: KittySdkConfig(
    projectId: 'proj_xxx',
    accountBaseUrl: '...',
    flareBaseUrl: '...',
    theme: KittyTheme(
      themeData: ThemeData(
        useMaterial3: true,
        fontFamily: 'DM Sans',
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.purple),
      ),
    ),
  ),
);

Customize auth and bind-email screens separately:

final authTheme = KittyAuthTheme(
  backgroundColor: Colors.white,
  loginHeaderWidget: Image.asset('assets/logo.png', height: 64),
  codeHeaderWidget: Image.asset('assets/logo.png', height: 48),
  loginBodyPadding: EdgeInsets.symmetric(horizontal: 24),
  primaryButtonStyle: ElevatedButton.styleFrom(
    minimumSize: Size(double.infinity, 48),
    shape: StadiumBorder(),
  ),
  inputDecoration: InputDecorationTheme(
    border: OutlineInputBorder(
      borderRadius: BorderRadius.all(Radius.circular(16)),
    ),
  ),
);

KittyAuthFlow(
  account: KittySdk.account,
  theme: authTheme,
);

KittyBindEmailFlow(
  account: KittySdk.account,
  theme: authTheme,
);

Customize settings screens by logical blocks:

KittySettingsFlow(
  account: KittySdk.account,
  onLoginPressed: () {},
  onRenewSubscription: () {},
  theme: KittySettingsTheme(
    page: KittySettingsPageTheme(
      backgroundColor: Colors.white,
      contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 24),
    ),
    menuItem: KittySettingsMenuItemTheme(
      borderRadius: BorderRadius.all(Radius.circular(16)),
      padding: EdgeInsets.symmetric(horizontal: 16, vertical: 14),
      iconColor: Colors.black54,
    ),
    buttons: KittySettingsButtonsTheme(
      primaryButtonSize: Size(240, 52),
      primaryButtonStyle: ElevatedButton.styleFrom(
        shape: StadiumBorder(),
      ),
    ),
    dialog: KittySettingsDialogTheme(
      destructiveActionStyle: TextStyle(color: Colors.red),
    ),
    bindEmail: KittySettingsBindEmailTheme(
      authTheme: authTheme,
    ),
  ),
);

All theme fields are optional. If a field is null, the SDK uses its default style from Theme.of(context) / the built-in Kitty UI.

page.actionIconTheme is opt-in and applies to every page inside KittySettingsFlow (a standalone KittyManageDataPage does not apply it). null keeps Flutter's back button. Use it to draw your own back button:

KittySettingsTheme(
  page: KittySettingsPageTheme(
    actionIconTheme: ActionIconThemeData(
      backButtonIconBuilder: (context) => const CircleAvatar(
        radius: 20,
        backgroundColor: Colors.white,
        child: Icon(Icons.arrow_back, color: Colors.black87),
      ),
    ),
  ),
);

Opt-in. It asks the user before photos, videos or prompts go to a third-party AI provider, and lets them withdraw later in Settings → Data (Apple App Review Guidelines 5.1.2(i) and 5.1.1(ii)). Pass a KittyConsentConfig to initialize:

await KittySdk.initialize(
  config: KittySdkConfig(
    projectId: 'proj_xxx',
    accountBaseUrl: '...',
    flareBaseUrl: '...',
  ),
  consentConfig: const KittyConsentConfig(
    providerName: 'fal.ai',
    appName: 'HoldStill',
    privacyPolicyUrl: 'https://example.com/privacy',
    termsOfUseUrl: 'https://example.com/terms',
    // policyVersion: 'ai-v1', // change it to ask every user again
  ),
);

initialize waits for the first storage read, so KittySdk.consent.status is ready when it returns, for the identity known at startup (usually guest, or the cached account). Until the signed-in account has loaded, a signed-in user reads as that startup identity, which is the fail-closed side. To avoid prompting a signed-in user under the guest identity, call promptIfUnknown after the account has loaded. Without a consentConfig nothing changes: KittySdk.isConsentEnabled is false, settings shows no Data row, and KittySdk.consent throws a StateError.

The SDK owns the consent record and its dialogs; the host blocks the AI work. The host makes three calls:

// 1. Home screen, once (from initState or a post-frame callback, never from
//    build): the blocking first-visit dialog, shown only while the status
//    is unknown.
await KittySdk.consent.promptIfUnknown(context);

// 2. Before every user-initiated AI action: true at once when granted,
//    otherwise the "Enable AI to create your photo" dialog. True only after
//    "Agree & Generate" has been saved.
if (!await KittySdk.consent.ensureConsent(context)) return;

// 3. In every repository that sends data to the AI provider: check again,
//    right before the request.
class UploadRepository {
  Future<void> upload(File photo) async {
    if (!KittySdk.consent.isGranted) throw const AiConsentRequiredException();
    // ... send to fal.ai
  }
}

Concurrent ensureConsent calls are not deduplicated: two taps can open two dialogs. Disable the Generate button while ensureConsent is pending, or guard it with an in-flight flag.

The cubit that calls the repository catches AiConsentRequiredException, calls ensureConsent, and retries only after a fresh user action. The check in the repository matters: consent can be withdrawn while a generation is in flight, and requests already sent cannot be recalled, so only a per-request check stops the next upload or retry. To react to changes, listen to KittySdk.consent.changes: it emits the current AiConsentStatus (unknown, granted, declined) to each new listener, then every change.

Fail closed. Anything uncertain counts as not granted:

  • no record, a corrupt record, a storage error (for example blocked localStorage on web), or a record saved under another policyVersion all read as unknown;
  • the status is derived on every read from the live identity's stored record, so a login, logout or account switch changes it at once, with no loading gap; a granted save counts only once the platform has confirmed it;
  • during a login, until the account fetch completes (the new session is stored but KittySdk.account.currentAccount is still null), the identity is still guest, so the guest's answer applies, a guest granted included; the signed-in account's own record applies as soon as the account has loaded;
  • a failed save of "Agree" inside a dialog leaves the status unchanged, shows saveErrorText and keeps the dialog open so the user can retry; only a direct KittySdk.consent.grant(...) call throws KittyConsentWriteException;
  • a failed withdrawal is never rolled back: the status stays declined, the stored key is removed so a restart cannot read a stale granted, and the write is retried with the next one (in this session only; the retry is not persisted);
  • if the identity changes while a dialog is open, the answer is saved for the identity that opened it, and ensureConsent returns false.

Storage scope. Consent is stored on the device only, in SharedPreferences (localStorage on web), under kitty_ai_consent_<projectId>_<accountId|guest>. There is no backend call and no sync: each Kitty account has its own record on each device, and withdrawing on one device does not affect another. Consent never moves between identities, so a guest's answer does not carry over after login. Logout keeps the record, so the same user signing back in keeps their choice.

Account deletion. Deleting the account through KittySettingsFlow removes that account's record automatically. Calling KittyAccount.deleteAccount() or markForDeletion() directly, outside the settings flow, does not clear the consent key. Deletion is a reversible mark (the account can be restored), so a user who signs back in and restores would get their old choice back, a granted included. Capture the account id, clear its record, then delete:

final id = KittySdk.account.currentAccount?.id;
if (id != null) await KittySdk.consent.clearForAccount(id);
await KittySdk.account.deleteAccount();

Clearing first fails closed: if deletion is refused, the only cost is a re-prompt. Clearing after can leave a restorable account with granted if logout throws after the deletion mark.

Settings. With consent enabled, KittySettingsFlow shows a Data row after Legal Info, to guests too. It opens Manage Data: a switch that turns consent on directly and, when turned off, asks "Turn off AI features?" in a centred pop-up (Keep Enabled, Turn Off; top-anchored instead with KittyConsentTheme.turnOffDialogTopOffset) before declining. A tap outside the pop-up or the system back button keeps consent on. KittySettingsFlow(consent: ...) is optional and defaults to KittySdk.consent when the SDK was initialized with a consentConfig. A custom cabinet can push the page itself:

Navigator.of(context).push(MaterialPageRoute(
  builder: (_) => KittyManageDataPage(consent: KittySdk.consent),
));

Text and theme. Every string is a KittyConsentConfig field with an English default (firstVisitTitle, generateBody, toggleSubtitle, ...); strings may use the {appName} and {providerName} placeholders. Pass translated strings for other languages. Visual tokens live in KittyConsentTheme: give it to the settings flow through KittySettingsTheme(consent: ...), and to the dialogs directly.

The SDK ships the layout only. With no theme, every colour and text style comes from the host's Theme (ColorScheme, TextTheme, DialogTheme), so the default look is neutral; the SDK hardcodes no brand colour. Pass your brand's colours explicitly:

Field Applies to Default
cardColor Consent dialog and turn-off pop-up card Dialog background
barrierColor Scrim behind both dialogs DialogTheme.barrierColor
primaryGradient, primaryColor, primaryTextStyle Agree, Agree & Generate (gradient wins) primary, onPrimary
secondaryColor, secondaryTextStyle Decline, Keep Enabled surface, onSurface
outlineColor Decline / Keep Enabled border (buttons only) outlineVariant
destructiveColor, destructiveTextStyle Turn Off error, onError
toggleActiveGradient, toggleActiveColor Manage Data switch track when on (gradient wins) primary
toggleInactiveColor Manage Data switch track when off surfaceContainerHighest
toggleThumbColor Manage Data switch knob, on and off onPrimary (on) / outline (off)
toggleBorderColor Manage Data switch track border when off none (no border unless set)
titleStyle, bodyStyle, linkStyle Dialog text; Manage Data title and subtitle unless manageDataTitleStyle / manageDataSubtitleStyle are set (they win) TextTheme; the turn-off pop-up uses the Figma sizes (title 20 w700, body 16 w500 / 24, labels 16 w700)
cardRadius, buttonHeight Card corners, button height 24, 56 (at least 48)
paragraphSpacing Gap between body paragraphs (split on a blank line); first-visit and generate dialogs only. Paragraphs still read as one text to screen readers null: one text with blank lines
titleBodySpacing, bodyLinksSpacing, contentButtonsSpacing Title → body, body → links, text → buttons gaps; first-visit and generate dialogs only. Links keep a 48 tap target, so their visible gaps are larger 12, 8, 24
linkGradient Fill of the link text and underline (every stop must meet 4.5:1 against cardColor) none: linkStyle colour
manageDataPadding Manage Data page padding around the card page.contentPadding, else 16
manageDataCardColor Manage Data card fill cardColor, else surface
manageDataTitleStyle, manageDataSubtitleStyle Manage Data card title and subtitle titleStyle / bodyStyle, else TextTheme
turnOffDialogTopOffset Turn-off pop-up top, from the top of the safe area; never closer than 24 to either edge, moves up when it does not fit and scrolls when it fills the screen; ignored with turnOffDialogBuilder null: centred

The Manage Data card always pads its content by 16 and rounds by 16; it ignores the settings menuItem.padding and menuItem.borderRadius.

// Example values only: use your own brand colours.
const consentTheme = KittyConsentTheme(
  cardColor: Color(0xFFFFFFFF),
  barrierColor: Color(0x99000000),
  primaryGradient: LinearGradient(
    colors: [Color(0xFF6A5AE0), Color(0xFFB45AE0)],
  ),
  outlineColor: Color(0xFFE0E0E6),
  destructiveColor: Color(0xFFE5484D),
  toggleActiveGradient: LinearGradient(
    colors: [Color(0xFF6A5AE0), Color(0xFFB45AE0)],
  ),
  toggleInactiveColor: Color(0xFFE9E9EF),
  toggleThumbColor: Color(0xFFFFFFFF),
);

KittySettingsFlow(
  account: KittySdk.account,
  onLoginPressed: () {},
  onRenewSubscription: () {},
  theme: const KittySettingsTheme(consent: consentTheme),
);

await KittySdk.consent.promptIfUnknown(context, theme: consentTheme);
await KittySdk.consent.ensureConsent(context, theme: consentTheme);

Custom UI. Three optional builders on KittyConsentConfig replace the default UI, while the SDK keeps the state, persistence, busy guard, error handling, results and analytics. A builder gets an immutable controller that the SDK rebuilds on every change; call its methods, never the store.

Builder Controller Replaces
dialogBuilder KittyConsentDialogController: kind, config, isBusy, errorText; agree(), decline(), openPrivacyPolicy(), openTermsOfUse() The content of both dialogs. The SDK keeps the route, the barrier rule per kind and the result.
turnOffDialogBuilder KittyTurnOffDialogController: config; keepEnabled(), turnOff() The turn-off pop-up. Its output is the dialog route's child, so return a Dialog. The SDK keeps the route, the barrier (tap outside → false) and the result.
manageDataBuilder KittyManageDataController: config, status, isGranted; enable(), requestDisable(), disableWithoutConfirmation() The body of KittyManageDataPage, below the SDK app bar
KittyConsentConfig(
  providerName: 'fal.ai',
  appName: 'HoldStill',
  dialogBuilder: (context, c) => MyConsentCard(
    title: c.kind == KittyConsentDialogKind.firstVisit
        ? c.config.firstVisitTitle
        : c.config.generateTitle,
    error: c.errorText,
    onAgree: c.isBusy ? null : c.agree,
    onDecline: c.isBusy ? null : c.decline,
  ),
  manageDataBuilder: (context, c) => SwitchListTile(
    title: Text(c.config.toggleTitle),
    value: c.isGranted,
    onChanged: (on) => on ? c.enable() : c.requestDisable(),
  ),
);

requestDisable() shows the turn-off pop-up (from turnOffDialogBuilder or the default) and declines only on "Turn Off".

Analytics. Events go through onAnalyticsEvent: AiConsentShown, AiConsentGranted, AiConsentDeclined, AiConsentWithdrawn, ManageDataOpened, SettingsDataClicked, AiConsentStorageError and ConsentClearFailed. They carry the status, source and policy version, never images, prompts or emails.

Buy Now — web paywall with mobile prices #

Requires Common Module v13.4.0+ on the web side.

final products = <ProductDetails>{...}; // from InAppPurchase

router.push(MaterialPageRoute(
  builder: (_) => KittyWebBuyNowPage(
    url: 'https://your-domain.com/buy-now', // Kitty Flow url
    products: products,
    onPaymentSuccess: () {
      router.pop();
    },
  ),
));

Modules #

Module Access Description
Flare KittySdk.flare Device fingerprint identification & purchase attribution
Account KittySdk.account Auth, user management, licenses, subscriptions
Tokens KittySdk.tokens Token balance and operations (requires Account)
Buy Now KittySdk.buyNow Web paywall with mobile price bridging
AI data consent KittySdk.consent Local, per-account AI data-processing consent, its dialogs and Manage Data (opt-in via consentConfig)

Tokens — balance and operations #

// 1. Enable tokens in KittySdkConfig
await KittySdk.initialize(
  config: KittySdkConfig(
    projectId: 'proj_xxx',
    accountBaseUrl: '...',
    flareBaseUrl: '...',
    tokens: const KittyTokensConfig(isDebug: true),
  ),
);

// 2. Use the module (user must be logged in via KittySdk.account)
//    By default the SDK uses the first usable license of the current user.
//    Pass `licenseId:` explicitly when you need to target a specific license.
final balance = await KittySdk.tokens.getBalance();
print('Tokens: ${balance.balance}');

// Get operations history
final ops = await KittySdk.tokens.getOperations(skip: 0, limit: 20);

// Token mutations (add/remove) are server-side only — call them from your
// backend with an internal API key or a per-project `kap_` bearer token
// against `POST /api/internal/tokens/add` and `/api/internal/tokens/remove`.

// 3. Use the Cubit in your UI
BlocProvider(
  create: (_) => TokensCubit(tokens: KittySdk.tokens)..loadBalance(),
  child: BlocBuilder<TokensCubit, TokensState>(
    builder: (context, state) {
      if (state.isLoading) return const CircularProgressIndicator();
      return Text('Balance: ${state.tokenCount}');
    },
  ),
)

Backend token mutations — important notes

add/remove are server-side only (call them from your backend with a per-project kap_ bearer token). Before you build against them, mind these three behaviours (full guide: kitty-launch-account/docs/backend-token-api.md):

  1. /add and /remove hit DIFFERENT buckets. /add credits the persistent balance bucket; /remove debits the renewable subscriptionBalance bucket (which has no floor and can go negative). A naive "grant then spend" will not net out — for a "tokens per operation" model, confirm with the platform team which bucket you should be debiting.
  2. No idempotency on /add / /remove. Calling twice applies twice. Deduplicate on your side (fire exactly once per operation, e.g. guard by your own operation id).
  3. A kap_ token cannot read balance. GET /balance and GET /operations require the internal API key — a kap_ bearer gets 401 there. Decide how your backend will verify results (e.g. read balance in the client via the SDK, or request read access from the platform team).
0
likes
0
points
1.38k
downloads

Publisher

verified publisherkitty-dev.org

Weekly Downloads

Kitty Flutter SDK — device fingerprint attribution (Flare) plus authentication, user management, licenses and subscriptions (Account) for the Kitty Launch platform.

Repository (GitHub)
View/report issues

License

unknown (license)

Dependencies

connectivity_plus, darq, device_info_plus, flutter, flutter_bloc, flutter_inappwebview, http, in_app_purchase, in_app_purchase_android, in_app_purchase_platform_interface, in_app_purchase_storekit, meta, package_info_plus, posthog_flutter, share_plus, shared_preferences, sign_in_with_apple, url_launcher, uuid, web

More

Packages that depend on kitty_sdk