# Flutter

> Accept Devzity payments in a Flutter app using your own backend, url_launcher and a deep link.

**No Flutter plugin is published for Devzity yet** — but the integration is fully supported today. It uses the same secure pattern as [Android](https://docs.devzity.com/v1/android.md) and [iOS](https://docs.devzity.com/v1/ios.md), with standard packages, the `checkout_url` your backend creates, the system browser, and a deep link.

```text
Flutter app ──▶ your small backend ──(Devzity secret key)──▶ Devzity
     ▲                  │
     └─ checkout_url ───┘
launchUrl(checkout_url) ──▶ customer pays ──▶ myapp://payment-return?… ──▶ app
app ──▶ your backend ──▶ "is my order paid?"
```

**The Devzity secret key stays on your backend.** Never put it in a Flutter or Dart bundle — a compiled app can be decompiled. No backend yet? See [I only have an app](https://docs.devzity.com/v1/mobile-backend.md).

## The flow

1. The app sends an **order id** to your backend; the backend decides the amount and returns `checkout_url`.
2. Open it with [`url_launcher`](https://pub.dev/packages/url_launcher) (`LaunchMode.externalApplication`).
3. Your backend set the payment's `return_url` to your app's deep link. Register it as an Android intent filter / App Link and an iOS URL scheme / Universal Link, and listen with [`app_links`](https://pub.dev/packages/app_links).
4. When the link arrives, **ask your backend for the order's status.**

pay_button.dart:

```dart
// Flutter: the app never holds a Devzity secret key. It sends an ORDER ID to YOUR backend (the server
// decides the amount — see examples/mobile-backend), opens the checkout_url it returns with
// url_launcher, and asks YOUR backend for the order's real status when the customer returns through
// your deep link (app_links).
//
// pubspec.yaml: url_launcher, http, app_links
import 'dart:convert';

import 'package:app_links/app_links.dart';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:url_launcher/url_launcher.dart';

const backend = 'https://your-backend.example.com';
const orderId = 'order-1001'; // the order being paid

class PayButton extends StatefulWidget {
  const PayButton({super.key});

  @override
  State<PayButton> createState() => _PayButtonState();
}

class _PayButtonState extends State<PayButton> {
  String message = 'Ready';

  @override
  void initState() {
    super.initState();
    // Return link: myapp://payment-return?devzity_payment_id=…&devzity_status=PAID
    AppLinks().uriLinkStream.listen((uri) async {
      if (!uri.queryParameters.containsKey('devzity_status')) return;
      // devzity_status is only a HINT — never fulfil from it. Refresh the order from YOUR backend.
      setState(() => message = 'Checking your payment…');
      final response = await http.get(Uri.parse('$backend/orders/$orderId/status'));
      setState(() => message = 'Order status: ${jsonDecode(response.body)['status']}');
    });
  }

  Future<void> pay() async {
    final response = await http.post(Uri.parse('$backend/orders/$orderId/pay'));
    final checkoutUrl = jsonDecode(response.body)['checkout_url'] as String;
    await launchUrl(Uri.parse(checkoutUrl), mode: LaunchMode.externalApplication);
  }

  @override
  Widget build(BuildContext context) => Column(children: [
        ElevatedButton(onPressed: pay, child: const Text('Pay for $orderId')),
        Text(message),
      ]);
}
```

> **Warning:** `devzity_status` in the return link is a **hint**, not proof. Never fulfil or unlock anything from it — refresh the order from your backend. Closing the browser is not a failed payment.
