Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 17 additions & 5 deletions android/src/main/java/jp/pokepay/pokepay_sdk/PokepaySdkPlugin.java
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,18 @@ private Date stringToDate(String s) {
}
}

// requestId は任意項目。Dart から null が来た場合はネイティブ SDK に
// そのまま null を渡す (pokepaylib は request_id を送らない)。
// 形式チェックは Dart 側 (parameters/request_id.dart の validateRequestId)
// で行うため、ここでは null ガードのみ。想定外の値は
// IllegalArgumentException のまま ProcessingError として伝搬させる
// (null に落とすと iOS で問題になっている request_id の
// 黙った欠落を Android でも再現してしまう)。
private UUID parseRequestId(String rawRequestId) {
if (rawRequestId == null) return null;
return UUID.fromString(rawRequestId);
}

private TaskResult invokeMethod() {
try {
switch (call.method) {
Expand Down Expand Up @@ -383,7 +395,7 @@ private TaskResult invokeMethod() {
TransactionStrategy txStrategy = parseTxStrategy(rawStrategy);
Double amount = call.argument("amount");
String rawRequestId = call.argument("requestId");
UUID requestId = UUID.fromString(rawRequestId);
UUID requestId = parseRequestId(rawRequestId);
if (amount != null){
req = new CreateTransactionWithBill(billId, accountId, amount, couponId, txStrategy, requestId);
}else{
Expand All @@ -402,7 +414,7 @@ private TaskResult invokeMethod() {
String couponId = call.argument("couponId");
String rawStrategy = call.argument("tx_strategy");
String rawRequestId = call.argument("requestId");
UUID requestId = UUID.fromString(rawRequestId);
UUID requestId = parseRequestId(rawRequestId);
TransactionStrategy txStrategy = parseTxStrategy(rawStrategy);
Integer topupQuotaId = call.argument("topupQuotaId");
CreateTransactionWithCashtray req = new CreateTransactionWithCashtray(cashtrayId, accountId, couponId,txStrategy,requestId).topupQuotaId(topupQuotaId);
Expand All @@ -416,7 +428,7 @@ private TaskResult invokeMethod() {
String checkId = call.argument("checkId");
String accountId = call.argument("accountId");
String rawRequestId = call.argument("requestId");
UUID requestId = UUID.fromString(rawRequestId);
UUID requestId = parseRequestId(rawRequestId);
Integer topupQuotaId = call.argument("topupQuotaId");
CreateTransactionWithCheck req = new CreateTransactionWithCheck(checkId, accountId, requestId).topupQuotaId(topupQuotaId);
Pokepay.setEnv(env);
Expand All @@ -433,7 +445,7 @@ private TaskResult invokeMethod() {
final ObjectMapper mapper = JsonConverter.createObjectMapper();
Product[] products = mapper.readValue(productsString, Product[].class);
String rawRequestId = call.argument("requestId");
UUID requestId = UUID.fromString(rawRequestId);
UUID requestId = parseRequestId(rawRequestId);
Integer topupQuotaId = call.argument("topupQuotaId");
CreateTransactionWithCpm req = new CreateTransactionWithCpm(cpmToken, accountId, amount, products, requestId).topupQuotaId(topupQuotaId);
Pokepay.setEnv(env);
Expand Down Expand Up @@ -810,7 +822,7 @@ private TaskResult invokeMethod() {
TransactionStrategy txStrategy = parseTxStrategy(rawStrategy);
String privateMoneyId = call.argument("privateMoneyId");
String rawRequestId = call.argument("requestId");
UUID requestId = UUID.fromString(rawRequestId);
UUID requestId = parseRequestId(rawRequestId);
Pokepay.Client client;
UserTransaction userTransaction;

Expand Down
11 changes: 6 additions & 5 deletions lib/bank_api/transaction.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'dart:convert';

import '../parameters/transaction_strategy.dart';
import '../parameters/product.dart';
import '../parameters/request_id.dart';
import '../pokepay_sdk.dart';
import '../responses.dart';

Expand All @@ -25,7 +26,7 @@ extension TransactionAPI on PokepayAPI {
'amount': amount,
'couponId': couponId,
'tx_strategy': strategy.value,
'requestId': requestId,
'requestId': validateRequestId(requestId),
},
);
}
Expand All @@ -48,7 +49,7 @@ extension TransactionAPI on PokepayAPI {
'accountId': accountId,
'couponId': couponId,
'tx_strategy': strategy.value,
'requestId': requestId,
'requestId': validateRequestId(requestId),
'topupQuotaId': topupQuotaId,
},
);
Expand All @@ -68,7 +69,7 @@ extension TransactionAPI on PokepayAPI {
'accessToken': this.accessToken,
'checkId': checkId,
'accountId': accountId,
'requestId': requestId,
'requestId': validateRequestId(requestId),
'topupQuotaId': topupQuotaId,
},
);
Expand All @@ -92,7 +93,7 @@ extension TransactionAPI on PokepayAPI {
'accountId': accountId,
'amount': amount,
'products': jsonEncode(products),
'requestId': requestId,
'requestId': validateRequestId(requestId),
'topupQuotaId': topupQuotaId,
},
);
Expand Down Expand Up @@ -201,7 +202,7 @@ extension TransactionAPI on PokepayAPI {
{
'env': this.env.index,
'accessToken': this.accessToken,
'requestId': requestId,
'requestId': validateRequestId(requestId),
},
);
}
Expand Down
42 changes: 42 additions & 0 deletions lib/parameters/request_id.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import '../responses/error.dart';

/// A canonical RFC 4122 textual UUID in **lowercase**: 8-4-4-4-12 hex digits.
///
/// Two independent constraints force this exact shape:
///
/// * **Lowercase**, because the API only accepts lowercase `request_id`.
/// Both native SDKs already normalise on write -- Java's `UUID.toString()`
/// emits lowercase, and iOS has an explicit `pokepayRequestID` extension
/// (`uuidString.lowercased()`) because Foundation's `uuidString` is
/// uppercase. But `getTransactionByRequestId` sends the caller's string
/// through verbatim on both platforms, so an uppercase value would write
/// successfully and then be unreadable. Rejecting instead of normalising
/// keeps the caller's string byte-identical to what the server stores.
/// * **Strict 8-4-4-4-12**, because this is the intersection of the two
/// native parsers. Java's `UUID.fromString` is lenient and also accepts
/// e.g. "1-1-1-1-1"; iOS's `UUID(uuidString:)` requires the canonical
/// form. Validating to the intersection is what lets a single Dart-side
/// check keep both platforms in agreement.
final RegExp _requestIdPattern = RegExp(
r'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$');

/// Validates the optional `requestId` (idempotency key) and returns it
/// unchanged. `null` is allowed and means "no idempotency key".
///
/// Validating here rather than in the native plugins makes both platforms
/// behave identically by construction. Android's `UUID.fromString` rejects a
/// malformed value loudly, while iOS's `UUID(uuidString:)` yields nil, which
/// makes the native SDK omit `request_id` from the request body entirely --
/// the server then creates a NON-idempotent transaction, so a retry
/// double-charges the user with no error raised anywhere.
String? validateRequestId(String? requestId) {
if (requestId == null) return null;
if (!_requestIdPattern.hasMatch(requestId)) {
throw ProcessingError(
message: 'requestId must be a lowercase UUID in the 8-4-4-4-12 '
'hexadecimal form (e.g. "550e8400-e29b-41d4-a716-446655440000"), '
'but got "$requestId".',
);
}
return requestId;
}
3 changes: 2 additions & 1 deletion lib/pokepay_sdk.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import 'package:flutter/services.dart';
import 'package:logger/logger.dart';
import 'bank_api/private_money.dart';
import 'parameters/transaction_strategy.dart';
import 'parameters/request_id.dart';

import 'bank_api/bill.dart';
import 'bank_api/check.dart';
Expand Down Expand Up @@ -331,7 +332,7 @@ class PokepayClient {
'couponId': couponId,
'tx_strategy': strategy.value,
'privateMoneyId': this.privateMoneyId,
'requestId': requestId,
'requestId': validateRequestId(requestId),
});

return UserTransaction.fromJson(jsonDecode(json));
Expand Down
Loading