Skip to main content

kitty_sdk 0.13.2 copy "kitty_sdk: ^0.13.2" to clipboard
kitty_sdk: ^0.13.2 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.1

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.0 #

Use 0.13.1 rather than 0.13.0. 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 both versions.

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).
  • 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).
  • 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.

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.
  • 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.
  • A purchase processed while a login is still loading its account takes the signed-out path. From the moment a login stores its credentials until its account fetch succeeds (after a failed fetch, until refreshAccountInfo() succeeds), an iOS purchase is not claimed for the user who just signed in. The SDK auto-logs in with the purchase's own App Store account instead: the purchase is credited to that account, and its session replaces the credentials just stored. This already applied to a login from a signed-out state; since 0.13.0 it also applies when a login replaces a signed-in user. A follow-up release claims under the stored Kitty session instead.

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 — 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.

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

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