Skip to main content

ackbiz_billing 0.4.0 copy "ackbiz_billing: ^0.4.0" to clipboard
ackbiz_billing: ^0.4.0 copied to clipboard

Shared billing for every ACKBiz app: Google Play, User Choice Billing, and the ACKBiz alternative rail, behind one purchase call.

ackbiz_billing #

Shared billing for every ACKBiz app. One purchase() call covers Google Play, User Choice Billing, and the ACKBiz alternative rail; the app never branches on which one was used.

Specification: docs/billing/12-mobile-flutter.md in the ackbiz web repo. That document is the source of truth — this package implements it and does not restate it.

Status #

Core, transport and platform layer are written. The Android module compiles alongside RevenueCat — example/ exists to prove it, because flutter analyze never touches Kotlin, pub publish validates metadata rather than whether a plugin links, and a package that builds on its own can still be unresolvable in every app that would use it.

Nothing here has taken a payment yet. Compiling proves linkage; it proves nothing about whether a sheet opens, a token verifies, or a grant lands. That needs a device, license testers and a Play Console enrolment — TASK-I01 in docs/billing/20-build-plan.md.

Piece State
BillingRouter — one completer, one idempotency key done, tested
outcomeForStatus — status to outcome, incl. F-44 done, tested
DeviceIdentity — salted hash of ANDROID_ID done, tested
Entitlements — three sources plus an offline cache done, tested
AckBizApi — all eight endpoints done, tested against a fake sender
AndroidBillingTransport + the plugin compiles, never run on a device
RazorpayAlternativeFlow compiles, never taken a payment
ApiPlayVerifier, SecureTokenStore, PreferencesEntitlementCache done
Paywall UI, seat chooser, state picker the app's, not this package's

Putting it together #

The package draws nothing and decides nothing about your UI. Four things are yours to supply: an HTTP client, the state picker, RevenueCat, and the paywall.

// 1. HTTP. Your app already has a client; this is the adapter.
class _Sender implements AckHttpSender {
  final _client = http.Client();

  @override
  Future<AckResponse> send(AckRequest request) async {
    final response = await _client
        .send(http.Request(request.method, request.url)
          ..headers.addAll(request.headers)
          ..body = request.body ?? '')
        .timeout(const Duration(seconds: 30));

    final body = await response.stream.bytesToString();
    return AckResponse(
      statusCode: response.statusCode,
      body: body,
      // Lower-cased: the renewed-token header is read from here.
      headers: response.headers,
    );
  }
}

// 2. The API, and the device it speaks for.
final api = AckBizApi(
  baseUrl: Uri.parse('https://ackbiz.com'),
  appKey: const String.fromEnvironment('ACK_APP_KEY'),
  packageName: 'com.ackbiz.khetihisab',
  sender: _Sender(),
  tokens: const SecureTokenStore(),
);

final transport = AndroidBillingTransport();
final identity = DeviceIdentity(
  read: transport.rawAndroidId,
  salt: const String.fromEnvironment('ACK_DEVICE_SALT'),
);

api.deviceId = await identity.get();
await api.registerDevice(AckDeviceInfo(deviceId: await identity.get()));
await transport.connect();

// 3. Is this user Pro? RevenueCat is injected rather than depended on — your
//    paywall already has the SDK, and this package should not pin its version.
final entitlements = Entitlements(
  revenueCatSaysPro: () async =>
      (await Purchases.getCustomerInfo()).entitlements.active.containsKey('pro'),
  backendSaysPro: api.isEntitled,
  cache: const PreferencesEntitlementCache(),
);

// 4. The router.
final router = BillingRouter(
  transport: transport,
  alternative: RazorpayAlternativeFlow(
    api: api,
    askState: showIndianStatePicker,   // yours: it decides CGST/SGST vs IGST
  ),
  verifier: ApiPlayVerifier(
    api: api,
    basePlanFor: (productId) => basePlans[productId],
  ),
  newIdempotencyKey: () => const Uuid().v4(),
);
// 5. The profile screen, when your app has one.
await api.saveProfile(
  displayName: name,
  email: email,          // stored encrypted; never an identity key
  phone: phone,          // E.164, or the backend refuses it
  countryCode: 'IN',
  avatar: 'avatar_07',   // a preset id from YOUR set, never a URL
  clear: emptiedFields,  // absent leaves a field; empty erases it
);

Read the cache before the first frame, never a network call. isPro() may touch RevenueCat, then the backend, then disk — awaiting it before rendering is a white screen for as long as the slowest link takes. Render from the cached answer, refresh after the first frame, rebuild if it changed.

Full walkthrough, including what has to be configured in the admin panel and Play Console before any of this works: docs/billing/12a-mobile-integration-guide.md.

What this package requires of your app #

Android floors, inherited by anything that adds this package:

Why
minSdk 23 flutter_secure_storage 10.x, which needs Keystore AES-GCM
compileSdk 36 Same. A module cannot compile against less than its dependencies
Play Billing 8.x Declared as a floor. RevenueCat resolves it too, and Gradle takes the highest

If your app is below either SDK level, raise it in android/app/build.gradle.kts — this package cannot compile otherwise.

It expects to sit next to RevenueCat. Entitlements takes RevenueCat as an injected function rather than depending on purchases_flutter, so the version is yours to choose. That is also why example/ depends on it: the package compiling alone proves nothing about the configuration every real app is in, and three defects shipped through exactly that gap.

Configuration: local and live #

Four values, none of them committed. Pass them at build time:

flutter build appbundle --release \
  --dart-define=ACK_BASE_URL=https://ackbiz.com \
  --dart-define=ACK_APP_KEY=... \
  --dart-define=ACK_DEVICE_SALT=... \
  --dart-define=RC_PUBLIC_KEY=...
const baseUrl = String.fromEnvironment('ACK_BASE_URL');
const appKey  = String.fromEnvironment('ACK_APP_KEY');
const salt    = String.fromEnvironment('ACK_DEVICE_SALT');
Value From Local build
ACK_BASE_URL https://ackbiz.com Your machine's LAN address or a tunnel — not localhost
ACK_APP_KEY Admin panel, at onboarding. Shown once The local backend issues its own
ACK_DEVICE_SALT The backend's DEVICE_ID_SALT Must match the backend you point at
RC_PUBLIC_KEY RevenueCat dashboard Same key; RevenueCat has no local mode

ACK_DEVICE_SALT is not a secret in this app's hands — it ships inside every APK and can be extracted. Its job is to stop a leaked devices table being reversible to raw Android IDs on its own. Ship it without worrying; just never change it, for the reason below.

Razorpay keys are not in this list, deliberately. The publishable key arrives in the /checkout/create response and the secret never leaves the server, so switching between Razorpay test and live mode is a server-side change with no app release attached.

What you cannot test locally #

Google Play verifies a purchase token against a backend it can reach, so the Play rail is always an internal-track test against the real server. A debug sideload cannot buy anything, whatever the backend is set to.

The alternative (Razorpay) rail has no such constraint and works against a local backend, which makes it the one to develop against day to day.

Two things that will cost you if you change them #

The salt. DeviceIdentity hashes ANDROID_ID with a build-time salt. Changing it invalidates every stored device id at once: no seat matches, no purchase restores, and nothing errors — every user simply appears new. The backend refuses to boot when its half changes; this side has no such protection.

device_info_plus. Do not reach for androidInfo.id when wiring the platform read. That is Build.ID, the OS build number, identical on every device with the same firmware — so every user on a given ROM shares one identity and therefore everyone else's pro. Use android_id.

Before publishing #

pub publish --dry-run is not sufficient for this package, and has now missed the same class of bug twice. It validates metadata and archive shape; it does not check that the archive contains what pubspec.yaml promises.

0.2.0 went out declaring pluginClass: AckBizBillingPlugin with no android/ directory in the tarball, because .pubignore still excluded it from when the package was pure Dart. Zero warnings. Every consuming app failed its Gradle sync.

So the release check is a BUILD against the published archive, not a dry run:

# after publishing, in a throwaway app
flutter pub add ackbiz_billing
flutter build apk --debug

Confirming the file list before publishing is the cheap half of it:

flutter pub publish --dry-run 2>&1 | sed -n '/^├── android/,/^├── example/p'

That must show build.gradle, AndroidManifest.xml, AckBizBillingPlugin.kt and UcbBillingManager.kt, and must NOT show local.properties -- .pubignore REPLACES .gitignore rather than extending it, so anything ignored for git has to be repeated there or it ships.

Testing #

flutter test
flutter analyze

The core has no store dependencies, which is deliberate: purchases_flutter, razorpay_flutter, android_id and flutter_secure_storage all belong to the platform layer, and keeping them out is what lets this logic be tested without a device.

0
likes
0
points
256
downloads

Publisher

verified publisherackplus.com

Weekly Downloads

Shared billing for every ACKBiz app: Google Play, User Choice Billing, and the ACKBiz alternative rail, behind one purchase call.

Repository (GitHub)
View/report issues

Topics

#billing #in-app-purchase #subscriptions #google-play #revenuecat

License

unknown (license)

Dependencies

crypto, flutter, flutter_secure_storage, http, razorpay_flutter, shared_preferences

More

Packages that depend on ackbiz_billing

Packages that implement ackbiz_billing