Skip to content

Repository files navigation

appspro_sdk

Native Flutter widgets and a typed client for AppsPro carrier-billed subscriptions — BDApps direct carrier billing on Robi and Airtel in Bangladesh.

Your user enters their number, gets an SMS code, and is subscribed. No WebView.

final result = await AppsProSubscribeScreen.show(context);
if (result?.isSubscribed ?? false) unlockPremium();

Install

dependencies:
  appspro_sdk: ^0.1.0

Configure

Get your publishable key from the API & SDK tab of your app on appspro.dev. It is safe to ship in a binary. Your secret key is not — it authorises server-to-server calls and doubles as your webhook signing key, so it must never appear in an app.

void main() {
  AppsProClient.configure(publishableKey: 'pk_...');
  runApp(const MyApp());
}

Use it

A full screen

final result = await AppsProSubscribeScreen.show(context);

An inline card

AppsProSubscribeCard(
  onSuccess: (result) => context.go('/premium'),
  onError: (message) => debugPrint(message),
)

A gate

AppsProSubscriptionGate(
  builder: (context) => const PremiumScreen(),
  fallback: (context, subscribe) => Paywall(onTap: subscribe),
)

Cancellation

Both app stores expect a subscription to be cancellable inside the app that sold it.

await AppsProManageScreen.show(context);

Your own UI

SubscribeController is the state machine behind the widgets. Drive it directly and build whatever screens you like.

final controller = SubscribeController()..load();
controller.addListener(() => setState(() {}));

await controller.submitPhone('01712345678');
await controller.submitOtp('123456');

switch (controller.step) {
  case SubscribeStep.otp: // show the code field
  case SubscribeStep.success: // controller.result
  default: // ...
}

Theming

The widgets are Material 3 and read everything from Theme.of(context), so they look like your app rather than like a payment vendor. AppsProOptions covers the rest:

AppsProSubscribeCard(
  options: const AppsProOptions(
    subscribeLabel: 'Start my plan',
    showBranding: false,
  ),
)

Two things are always shown, because BDApps requires them before a user agrees to a recurring carrier charge: the price disclosure line and the app header naming who is charging them.

Routing through your own server

By default the package talks to AppsPro directly using the publishable key. If you already have a backend and a signed-in user, route calls through it instead: your server holds the secret key, so it can tie a subscription to your own user id and authorise and log every call.

AppsProClient.configure(
  publishableKey: 'pk_...',
  backend: AppsProProxyBackend(
    baseUrl: 'https://api.mystore.com',
    headers: () async => {'Authorization': 'Bearer ${await session.token()}'},
  ),
);

Your server implements five routes. The paths are a convention — rename them with AppsProProxyPaths.

Route Body Returns
GET /appspro/app-info the /api/v1/sdk/app-info payload
POST /appspro/otp/request {phone, external_user_id?} {reference_no, status_code, ...}
POST /appspro/otp/verify {reference_no, otp, external_user_id?} {subscriber_id, subscription_status, ...}
GET /appspro/subscription {valid, status, reason?}
POST /appspro/unsubscribe any 2xx

A reference proxy in Express:

const APPSPRO = 'https://api.appspro.dev';
const auth = { Authorization: `Bearer ${process.env.APPSPRO_SECRET_KEY}` };

app.get('/appspro/app-info', async (req, res) => {
  const r = await fetch(`${APPSPRO}/api/v1/sdk/app-info?publishable_key=${process.env.APPSPRO_PUBLISHABLE_KEY}`);
  res.status(r.status).json(await r.json());
});

app.post('/appspro/otp/request', requireLogin, async (req, res) => {
  const r = await fetch(`${APPSPRO}/api/v1/sdk/otp/request`, {
    method: 'POST',
    headers: { ...auth, 'Content-Type': 'application/json' },
    body: JSON.stringify({ phone: req.body.phone, external_user_id: req.user.id }),
  });
  res.status(r.status).json(await r.json());
});

// otp/verify mirrors the above against /api/v1/sdk/otp/verify.

app.get('/appspro/subscription', requireLogin, async (req, res) => {
  const r = await fetch(`${APPSPRO}/api/v1/sdk/verify/${req.user.subscriberId}`, { headers: auth });
  const body = await r.json();
  res.json({ valid: body.valid, status: body.subscriber?.status, reason: body.reason });
});

app.post('/appspro/unsubscribe', requireLogin, async (req, res) => {
  const r = await fetch(`${APPSPRO}/api/v1/sdk/unsubscribe`, {
    method: 'POST',
    headers: { ...auth, 'Content-Type': 'application/json' },
    body: JSON.stringify({ phone: req.user.phone }),
  });
  res.status(r.status).json(await r.json());
});

And in FastAPI:

APPSPRO = "https://api.appspro.dev"
AUTH = {"Authorization": f"Bearer {os.environ['APPSPRO_SECRET_KEY']}"}

@router.post("/appspro/otp/request")
async def request_otp(body: PhoneIn, user: User = Depends(current_user)):
    async with httpx.AsyncClient() as c:
        r = await c.post(
            f"{APPSPRO}/api/v1/sdk/otp/request",
            headers=AUTH,
            json={"phone": body.phone, "external_user_id": user.id},
        )
    return JSONResponse(r.json(), status_code=r.status_code)

You can also override just one call and leave the rest direct:

AppsProClient.configure(
  publishableKey: 'pk_...',
  statusResolver: () async => myBackend.subscriptionStatus(),
);

Testing

Sandbox apps talk to a mock carrier: no SMS, no charges, and the OTP is always 000000. Create one on the API & SDK tab, then:

flutter run --dart-define=APPSPRO_PUBLISHABLE_KEY=pk_sandbox_key

The sandbox app and its keys are deleted automatically when your app is published, so make sure your release build uses the production key.

In your own tests, implement AppsProBackend — no HTTP needed:

class FakeBackend implements AppsProBackend {
  @override
  Future<SubscriptionStatus> status() async =>
      const SubscriptionStatus(isSubscribed: true, state: SubscriptionState.active);
  // ...
}

AppsProClient.configure(publishableKey: 'pk_test', backend: FakeBackend());

Errors

Everything throws an AppsProException subclass, so you can tell retry from re-auth without matching on strings:

Exception Meaning
AppsProNetworkException Never reached the server. Retry.
AppsProInvalidPhoneException Not a valid BD mobile number.
AppsProInvalidOtpException Wrong or expired code.
AppsProRateLimitException 10 OTPs/hour per number exhausted.
AppsProUnauthorizedException No valid session; subscribe again.
AppsProNotConfiguredException The app has no BDApps credentials yet.
AppsProUnknownAppException The publishable key matches no app.

Notes

  • Access is a server decision. The gate reflects it, but verify entitlement on your own server before handing out anything valuable — a determined user controls their device.
  • subscriber.created fires when the carrier confirms, not at OTP verify, so a webhook may lag a successful subscribe by seconds or minutes. Status reports pending in that window and the SDK treats it as subscribed.
  • Bangladesh only. BDApps bills Robi and Airtel numbers; AppInfo.supportedCountries reports what the app can actually charge.

Links

About

Native Flutter SDK for AppsPro carrier-billed subscriptions (BDApps DCB — Robi/Airtel). Phone + OTP subscribe, status and in-app cancel, as Material 3 widgets that adapt to your app's theme.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages