feat(shopinbit): migrate to PUT /payment for 1.0.4
What changed, and why it matters
This commit updates the Stack Wallet app's integration with the ShopinBit service. It changes how a payment invoice is created: previously the app used a GET request (which could accidentally create invoices), and now it uses a PUT request after the user clicks 'PAY NOW'. This is a normal API migration to match a new ShopinBit specification version 1.0.4. There is no direct evidence in the commit of a security vulnerability being fixed or introduced.
No security action required based on this commit alone. Treat as a routine feature/API migration. If reviewing for security, verify that the new PUT endpoint on the server side requires proper authentication/authorization and that repeated PUT calls cannot be abused to invalidate payments or cause denial-of-wallet conditions.
Security signals we found
API method migration from GET to PUT for state-changing operation
Separation of read-only GET and mutating PUT endpoints
No input validation, authentication, or cryptography changes visible
No memory safety, injection, or authorization changes visible
Evidence from the diff
The diff adds a generic HTTP.put() method to the app’s HTTP client, then updates the ShopInBit client to split the previous getPayment() behavior into two methods: getPayment() becomes a read-only GET for polling/recovery, and putPayment() becomes a mutating PUT that creates or regenerates the BTCPay invoice. The UI’s payment view now calls putPayment() when loading from the shipping view and on retry. The change aligns the client with the ShopinBit 1.0.4 API contract where GET /payment no longer creates invoices. No security bug is visible in the diff; it is a feature/API migration.
Changed components
lib/networking/http.dartlib/pages/shopinbit/shopinbit_payment_view.dartlib/services/shopinbit/src/client.dartInspect captured patch +65 / −9
diff --git a/lib/networking/http.dart b/lib/networking/http.dart
index efa997e..246891d 100644
--- a/lib/networking/http.dart
+++ b/lib/networking/http.dart
@@ -87,6 +87,37 @@ class HTTP {
}
}
+ Future<Response> put({
+ required Uri url,
+ Map<String, String>? headers,
+ Object? body,
+ required ({InternetAddress host, int port})? proxyInfo,
+ }) async {
+ final httpClient = HttpClient();
+ try {
+ if (proxyInfo != null) {
+ SocksTCPClient.assignToHttpClient(httpClient, [
+ ProxySettings(proxyInfo.host, proxyInfo.port),
+ ]);
+ }
+ final HttpClientRequest request = await httpClient.putUrl(url);
+
+ if (headers != null) {
+ headers.forEach((key, value) => request.headers.add(key, value));
+ }
+
+ if (body != null) request.write(body);
+
+ final response = await request.close();
+ return Response(await _bodyBytes(response), response.statusCode);
+ } catch (e, s) {
+ Logging.instance.w("HTTP.put() rethrew: ", error: e, stackTrace: s);
+ rethrow;
+ } finally {
+ httpClient.close(force: true);
+ }
+ }
+
Future<Response> patch({
required Uri url,
Map<String, String>? headers,
diff --git a/lib/pages/shopinbit/shopinbit_payment_view.dart b/lib/pages/shopinbit/shopinbit_payment_view.dart
index eea1f43..23afcb3 100644
--- a/lib/pages/shopinbit/shopinbit_payment_view.dart
+++ b/lib/pages/shopinbit/shopinbit_payment_view.dart
@@ -121,13 +121,15 @@ class _ShopInBitPaymentViewState extends ConsumerState<ShopInBitPaymentView> {
} catch (_) {}
}
+ // Entered from the shipping view's PAY NOW button: create the invoice
+ // via PUT per the 1.0.4 spec. GET no longer creates invoices.
Future<void> _loadPayment() async {
setState(() => _loading = true);
try {
final resp = await ref
.read(pShopinBitService)
.client
- .getPayment(widget.model.apiTicketId);
+ .putPayment(widget.model.apiTicketId);
if (!resp.hasError && resp.value != null) {
_applyPaymentInfo(resp.value!);
}
@@ -147,7 +149,7 @@ class _ShopInBitPaymentViewState extends ConsumerState<ShopInBitPaymentView> {
final resp = await ref
.read(pShopinBitService)
.client
- .getPayment(widget.model.apiTicketId, retry: true);
+ .putPayment(widget.model.apiTicketId);
if (!resp.hasError && resp.value != null) {
_applyPaymentInfo(resp.value!);
}
diff --git a/lib/services/shopinbit/src/client.dart b/lib/services/shopinbit/src/client.dart
index a1f9b8b..ad695e2 100644
--- a/lib/services/shopinbit/src/client.dart
+++ b/lib/services/shopinbit/src/client.dart
@@ -292,13 +292,29 @@ class ShopInBitClient {
// -- Payment --
- Future<ApiResponse<PaymentInfo>> getPayment(
- int ticketId, {
- bool retry = false,
- }) async {
- final path = '/tickets/$ticketId/payment';
- final query = retry ? {'retry': 'true'} : null;
- return _request('GET', path, query: query, parse: PaymentInfo.fromJson);
+ /// Read existing invoice state. Use this for polling, page-reload recovery,
+ /// and any view that just wants to show the current invoice; per ShopinBit
+ /// 1.0.4 this endpoint is read-only and will not create or regenerate the
+ /// invoice. Call [putPayment] for that.
+ Future<ApiResponse<PaymentInfo>> getPayment(int ticketId) async {
+ return _request(
+ 'GET',
+ '/tickets/$ticketId/payment',
+ parse: PaymentInfo.fromJson,
+ );
+ }
+
+ /// Create or regenerate the BTCPay invoice for [ticketId]. Per the 1.0.4
+ /// spec call this only after the customer has accepted the offer, submitted
+ /// shipping/billing, seen the Terms & Conditions, and explicitly clicked
+ /// PAY NOW. Repeated calls regenerate the invoice and invalidate any in-
+ /// flight payment.
+ Future<ApiResponse<PaymentInfo>> putPayment(int ticketId) async {
+ return _request(
+ 'PUT',
+ '/tickets/$ticketId/payment',
+ parse: PaymentInfo.fromJson,
+ );
}
// -- Vouchers --
@@ -547,6 +563,13 @@ class ShopInBitClient {
body: body != null ? _asciiSafeJson(body) : null,
proxyInfo: proxy,
);
+ case 'PUT':
+ return _httpClient.put(
+ url: uri,
+ headers: headers,
+ body: body != null ? jsonEncode(body) : null,
+ proxyInfo: proxy,
+ );
case 'PATCH':
return _httpClient.patch(
url: uri,
Why this scored 12/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.