diff --git a/README.md b/README.md index afc7d57..4b8f1ef 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,9 @@ If you are an AI agent implementing this SDK, follow the steps below exactly. If If they are on the receive side, ask one follow-up: - **Platform** — single-tenant, one API key. -- **Parent Platform** — multi-tenant, manages multiple child platforms each with their own API key and HMAC secret. +- **Parent Platform** — multi-tenant, manages multiple child platforms. Two variants: + - **Per-client keys** — each child has its own API key and HMAC secret; you scope requests per-call. + - **Shared key** — one API key and HMAC secret for all children; tag the child per-payment with `setChildPlatform()`. **2. Follow the matching Quick Start section below.** @@ -47,9 +49,13 @@ Receive side (platforms): - Always call `.setZk()` on the `PaymentBuilder` before calling `addPaymentAsync`. Plain-text destinations are rejected in `strict` mode. - Store the `secret` from `AddPaymentResult` alongside the invoice — it is required to reconstruct the verify URL for the wallet. -Receive side (parent platforms), in addition to the platform rules: -- Include `hmacSecret` in `BrantaClientOptions`. -- Pass per-call `BrantaClientOptions` to scope requests to the correct child platform's API key. +Receive side (parent platforms — per-client keys), in addition to the platform rules: +- Include `hmacSecret` in `BrantaClientOptions` but omit `defaultApiKey` at service setup. +- Pass per-call `BrantaClientOptions` with each child's API key to scope requests. + +Receive side (parent platforms — shared key), in addition to the platform rules: +- Include both `defaultApiKey` and `hmacSecret` in `BrantaClientOptions`. +- Call `.setChildPlatform(name, logoUrl: ..., logoLightUrl: ...)` on the builder to tag each payment with the child's branding. # Quick Start @@ -155,7 +161,12 @@ final result = await service.addPaymentAsync(payment); ## For Parent Platforms (Receive Side) -Parent platforms sign requests with HMAC in addition to the API key. Pass per-call `BrantaClientOptions` to scope requests to each child platform. +Parent platforms sign requests with HMAC. Choose a variant based on how API keys are structured. + +
+Shared key — one API key covers all children (Recommended) + +Set up with a single API key and HMAC secret; identify the child platform per-payment. ```dart import 'package:branta/branta.dart'; @@ -163,7 +174,7 @@ import 'package:http/http.dart' as http; final options = BrantaClientOptions( baseUrl: BrantaServerBaseUrl.staging, - defaultApiKey: '', + defaultApiKey: '', hmacSecret: '', privacy: PrivacyMode.strict, ); @@ -178,12 +189,52 @@ final payment = PaymentBuilder() .setDescription('Order #1234') .addDestination('1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa') .setZk() + .setChildPlatform('ChildBrand', logoUrl: 'https://example.com/logo.png') .setTtl(600) .build(); final result = await service.addPaymentAsync(payment); ``` +
+ +
+Per-client keys — each child has its own API key + +Set up the service with the shared HMAC secret only; pass each child's API key per-call. + +```dart +import 'package:branta/branta.dart'; +import 'package:http/http.dart' as http; + +final options = BrantaClientOptions( + baseUrl: BrantaServerBaseUrl.staging, + hmacSecret: '', + privacy: PrivacyMode.strict, +); +final brantaClient = BrantaClient(httpClient: http.Client(), defaultOptions: options); +final service = BrantaService( + client: brantaClient, + aesEncryption: AesEncryptionService(), + defaultOptions: options, +); + +final payment = PaymentBuilder() + .setDescription('Order #1234') + .addDestination('1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa') + .setZk() + .setTtl(600) + .build(); + +// Scope to the child platform's API key per-call +final result = await service.addPaymentAsync( + payment, + options: BrantaClientOptions(defaultApiKey: ''), +); +``` + +
+ # Privacy `PrivacyMode` controls whether plain-text on-chain lookups are allowed. diff --git a/lib/src/v2/classes/payment_builder.dart b/lib/src/v2/classes/payment_builder.dart index 45260e7..08b11fc 100644 --- a/lib/src/v2/classes/payment_builder.dart +++ b/lib/src/v2/classes/payment_builder.dart @@ -3,6 +3,7 @@ import 'package:uuid/uuid.dart'; import '../../enums/destination_type.dart'; import '../models/destination.dart'; import '../models/payment.dart'; +import '../models/platform.dart'; class PaymentBuilder { final Payment _payment = Payment(destinations: []); @@ -50,5 +51,10 @@ class PaymentBuilder { return this; } + PaymentBuilder setChildPlatform(String name, {String? logoUrl, String? logoLightUrl}) { + _payment.childPlatform = Platform(name: name, logoUrl: logoUrl, logoLightUrl: logoLightUrl); + return this; + } + Payment build() => _payment; } diff --git a/lib/src/v2/models/payment.dart b/lib/src/v2/models/payment.dart index fdb2d94..0fbc1b3 100644 --- a/lib/src/v2/models/payment.dart +++ b/lib/src/v2/models/payment.dart @@ -11,6 +11,7 @@ class Payment { String? platformLogoUrl; String? platformLogoLightUrl; Platform? parentPlatform; + Platform? childPlatform; String? btcPayServerPluginVersion; /// Runtime-only flag set by [BrantaService] after metadata decryption. @@ -27,6 +28,7 @@ class Payment { this.platformLogoUrl, this.platformLogoLightUrl, this.parentPlatform, + this.childPlatform, this.btcPayServerPluginVersion, }); @@ -67,6 +69,7 @@ class Payment { if (platformLogoLightUrl != null) 'platform_logo_light_url': platformLogoLightUrl, if (parentPlatform != null) 'parent_platform': parentPlatform!.toJson(), + if (childPlatform != null) 'child_platform': childPlatform!.toJson(), if (btcPayServerPluginVersion != null) 'btc_pay_server_plugin_version': btcPayServerPluginVersion, }; diff --git a/test/branta_test.dart b/test/branta_test.dart index d75a699..e2d786f 100644 --- a/test/branta_test.dart +++ b/test/branta_test.dart @@ -670,6 +670,68 @@ void main() { final dest = Destination(value: 'addr'); expect(dest.toJson().containsKey('type'), isFalse); }); + + test('setChildPlatform sets name', () { + final payment = PaymentBuilder() + .addDestination(_bitcoinAddress) + .setChildPlatform('Acme') + .build(); + + expect(payment.childPlatform!.name, equals('Acme')); + }); + + test('setChildPlatform optional URLs default to null', () { + final payment = PaymentBuilder() + .addDestination(_bitcoinAddress) + .setChildPlatform('Acme') + .build(); + + expect(payment.childPlatform!.logoUrl, isNull); + expect(payment.childPlatform!.logoLightUrl, isNull); + }); + + test('setChildPlatform with URLs sets urls', () { + final payment = PaymentBuilder() + .addDestination(_bitcoinAddress) + .setChildPlatform('Acme', + logoUrl: 'https://example.com/logo.png', + logoLightUrl: 'https://example.com/logo-light.png') + .build(); + + expect(payment.childPlatform!.logoUrl, equals('https://example.com/logo.png')); + expect(payment.childPlatform!.logoLightUrl, equals('https://example.com/logo-light.png')); + }); + + test('setChildPlatform returns builder', () { + final builder = PaymentBuilder().addDestination(_bitcoinAddress); + final result = builder.setChildPlatform('Acme'); + + expect(result, same(builder)); + }); + + test('setChildPlatform serializes to toJson', () { + final payment = PaymentBuilder() + .addDestination(_bitcoinAddress) + .setChildPlatform('Acme', logoUrl: 'https://example.com/logo.png') + .build(); + + final json = payment.toJson(); + final cp = json['child_platform'] as Map; + + expect(cp['name'], equals('Acme')); + expect(cp['logo_url'], equals('https://example.com/logo.png')); + expect(cp.containsKey('logo_light_url'), isFalse); + }); + + test('no childPlatform omitted from toJson', () { + final payment = PaymentBuilder() + .addDestination(_bitcoinAddress) + .build(); + + final json = payment.toJson(); + + expect(json.containsKey('child_platform'), isFalse); + }); }); // -------------------------------------------------------------------------