feat: integrate enakgame and voucher
Build & Deploy iOS to TestFlight / build-and-deploy (push) Waiting to run
Build & Deploy iOS to TestFlight / build-and-deploy (push) Waiting to run
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:data_channel/data_channel.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
|
||||
import '../../../common/api/api_client.dart';
|
||||
import '../../../common/api/api_failure.dart';
|
||||
import '../../../common/api/interceptors/sensitive_aware_log_interceptor.dart';
|
||||
import '../../../common/function/app_function.dart';
|
||||
import '../../../common/url/api_path.dart';
|
||||
import '../../../domain/wallet/wallet.dart';
|
||||
import '../wallet_dtos.dart';
|
||||
import '../wallet_response.dart';
|
||||
|
||||
/// Katalog, tukar, dan riwayat voucher (§9).
|
||||
@injectable
|
||||
class VoucherRemoteDataProvider {
|
||||
final ApiClient _apiClient;
|
||||
final String _logName = 'VoucherRemoteDataProvider';
|
||||
|
||||
VoucherRemoteDataProvider(this._apiClient);
|
||||
|
||||
Future<DC<WalletFailure, List<VoucherDto>>> fetchVouchers() => _request(
|
||||
'fetchVouchers',
|
||||
() => _apiClient.get(ApiPath.vouchers, headers: getAuthorizationHeader()),
|
||||
(response) => ((response.data as Map)['data'] as List? ?? [])
|
||||
.map((e) => VoucherDto.fromJson(Map<String, dynamic>.from(e as Map)))
|
||||
.toList(),
|
||||
);
|
||||
|
||||
Future<DC<WalletFailure, VoucherRedemptionDto>> redeem({
|
||||
required String voucherId,
|
||||
required String pin,
|
||||
required String idempotencyKey,
|
||||
}) => _request(
|
||||
'redeem',
|
||||
() => _apiClient.post(
|
||||
ApiPath.voucherRedeem(voucherId),
|
||||
headers: {...getAuthorizationHeader(), 'Idempotency-Key': idempotencyKey},
|
||||
data: {'pin': pin},
|
||||
extra: {kSensitiveRequest: true},
|
||||
),
|
||||
(response) => VoucherRedemptionDto.fromJson(walletDataOf(response)),
|
||||
);
|
||||
|
||||
Future<DC<WalletFailure, VoucherRedemptionPageDto>> fetchRedemptions({
|
||||
required int page,
|
||||
required int limit,
|
||||
}) => _request(
|
||||
'fetchRedemptions',
|
||||
() => _apiClient.get(
|
||||
ApiPath.voucherRedemptions,
|
||||
headers: getAuthorizationHeader(),
|
||||
params: {'page': page, 'limit': limit},
|
||||
),
|
||||
_parseRedemptionPage,
|
||||
);
|
||||
|
||||
/// `data` bisa berupa list langsung dengan `pagination` di envelope, atau
|
||||
/// objek `{ data, pagination }`.
|
||||
VoucherRedemptionPageDto _parseRedemptionPage(Response response) {
|
||||
final body = response.data as Map;
|
||||
final payload = body['data'];
|
||||
final rawItems = payload is Map ? payload['data'] : payload;
|
||||
final rawPagination = payload is Map
|
||||
? payload['pagination']
|
||||
: body['pagination'];
|
||||
|
||||
return VoucherRedemptionPageDto(
|
||||
items: (rawItems as List? ?? [])
|
||||
.map(
|
||||
(e) => VoucherRedemptionDto.fromJson(
|
||||
Map<String, dynamic>.from(e as Map),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
pagination: rawPagination is Map
|
||||
? WalletPaginationDto.fromJson(
|
||||
Map<String, dynamic>.from(rawPagination),
|
||||
)
|
||||
: const WalletPaginationDto(),
|
||||
);
|
||||
}
|
||||
|
||||
Future<DC<WalletFailure, T>> _request<T>(
|
||||
String name,
|
||||
Future<Response> Function() call,
|
||||
T Function(Response response) parse,
|
||||
) async {
|
||||
try {
|
||||
final response = await call();
|
||||
final failure = parseWalletFailure(response);
|
||||
if (failure != null) return DC.error(failure);
|
||||
return DC.data(parse(response));
|
||||
} on ApiFailure catch (e, s) {
|
||||
log(name, name: _logName, error: e, stackTrace: s);
|
||||
return DC.error(WalletFailure.serverError(e));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -60,19 +60,6 @@ class WalletRemoteDataProvider {
|
||||
(response) => WalletExpiringDto.fromJson(walletDataOf(response)),
|
||||
);
|
||||
|
||||
Future<DC<WalletFailure, PaymentCodeDto>> createPaymentCode({
|
||||
required String pin,
|
||||
}) => _request(
|
||||
'createPaymentCode',
|
||||
() => _apiClient.post(
|
||||
ApiPath.walletPaymentCode,
|
||||
headers: getAuthorizationHeader(),
|
||||
data: {'pin': pin},
|
||||
extra: {kSensitiveRequest: true},
|
||||
),
|
||||
(response) => PaymentCodeDto.fromJson(walletDataOf(response)),
|
||||
);
|
||||
|
||||
Future<DC<WalletFailure, ExchangePreviewDto>> previewExchange({
|
||||
required int coins,
|
||||
}) => _request(
|
||||
|
||||
@@ -100,7 +100,6 @@ abstract class CustomerOrderPaymentDto with _$CustomerOrderPaymentDto {
|
||||
@JsonKey(name: 'method_type') String? methodType,
|
||||
@JsonKey(name: 'amount') int? amount,
|
||||
@JsonKey(name: 'refund_amount') int? refundAmount,
|
||||
@JsonKey(name: 'points_used') int? pointsUsed,
|
||||
}) = _CustomerOrderPaymentDto;
|
||||
|
||||
factory CustomerOrderPaymentDto.fromJson(Map<String, dynamic> json) =>
|
||||
@@ -114,7 +113,6 @@ abstract class CustomerOrderPaymentDto with _$CustomerOrderPaymentDto {
|
||||
methodType: methodType ?? '',
|
||||
amount: amount ?? 0,
|
||||
refundAmount: refundAmount ?? 0,
|
||||
pointsUsed: pointsUsed,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ abstract class OutletDto with _$OutletDto {
|
||||
@JsonKey(name: 'id') String? id,
|
||||
@JsonKey(name: 'name') String? name,
|
||||
@JsonKey(name: 'address') String? address,
|
||||
@JsonKey(name: 'accepts_point_payment') bool? acceptsPointPayment,
|
||||
@JsonKey(name: 'earns_points') bool? earnsPoints,
|
||||
@JsonKey(name: 'earns_coins') bool? earnsCoins,
|
||||
}) = _OutletDto;
|
||||
@@ -20,7 +19,6 @@ abstract class OutletDto with _$OutletDto {
|
||||
id: id ?? '',
|
||||
name: name ?? '',
|
||||
address: (address?.trim().isEmpty ?? true) ? null : address!.trim(),
|
||||
acceptsPointPayment: acceptsPointPayment ?? false,
|
||||
earnsPoints: earnsPoints ?? false,
|
||||
earnsCoins: earnsCoins ?? false,
|
||||
);
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
part of '../wallet_dtos.dart';
|
||||
|
||||
@freezed
|
||||
abstract class PaymentCodeDto with _$PaymentCodeDto {
|
||||
const factory PaymentCodeDto({
|
||||
@JsonKey(name: 'code') String? code,
|
||||
@JsonKey(name: 'qr_payload') String? qrPayload,
|
||||
@JsonKey(name: 'expires_at') String? expiresAt,
|
||||
}) = _PaymentCodeDto;
|
||||
|
||||
factory PaymentCodeDto.fromJson(Map<String, dynamic> json) =>
|
||||
_$PaymentCodeDtoFromJson(json);
|
||||
|
||||
const PaymentCodeDto._();
|
||||
|
||||
PaymentCode toDomain() => PaymentCode(
|
||||
code: code ?? '',
|
||||
qrPayload: qrPayload ?? '',
|
||||
// Kode berlaku 2 menit; dipakai hanya bila server tidak mengirim expires_at.
|
||||
expiresAt:
|
||||
_parseDateTime(expiresAt) ??
|
||||
DateTime.now().add(const Duration(minutes: 2)),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
part of '../wallet_dtos.dart';
|
||||
|
||||
/// `terms` belum dibaca: bentuknya masih disepakati dengan tim backoffice,
|
||||
/// sementara itu yang ditampilkan `description` saja (§9.1).
|
||||
@freezed
|
||||
abstract class VoucherDto with _$VoucherDto {
|
||||
const factory VoucherDto({
|
||||
@JsonKey(name: 'id') String? id,
|
||||
@JsonKey(name: 'name') String? name,
|
||||
@JsonKey(name: 'description') String? description,
|
||||
@JsonKey(name: 'image_url') String? imageUrl,
|
||||
@JsonKey(name: 'voucher_type') String? voucherType,
|
||||
@JsonKey(name: 'face_value') int? faceValue,
|
||||
@JsonKey(name: 'point_cost') int? pointCost,
|
||||
@JsonKey(name: 'max_per_customer') int? maxPerCustomer,
|
||||
@JsonKey(name: 'valid_until') String? validUntil,
|
||||
@JsonKey(name: 'available') int? available,
|
||||
}) = _VoucherDto;
|
||||
|
||||
factory VoucherDto.fromJson(Map<String, dynamic> json) =>
|
||||
_$VoucherDtoFromJson(json);
|
||||
|
||||
const VoucherDto._();
|
||||
|
||||
Voucher toDomain() => Voucher(
|
||||
id: id ?? '',
|
||||
name: name ?? '',
|
||||
description: (description?.trim().isEmpty ?? true) ? null : description,
|
||||
imageUrl: (imageUrl?.isEmpty ?? true) ? null : imageUrl,
|
||||
type: VoucherType.fromApi(voucherType),
|
||||
faceValue: faceValue ?? 0,
|
||||
pointCost: pointCost ?? 0,
|
||||
maxPerCustomer: maxPerCustomer,
|
||||
validUntil: _parseDateTime(validUntil),
|
||||
available: available,
|
||||
);
|
||||
}
|
||||
|
||||
/// Item "Voucher saya" dan response tukar voucher; [pointBalance] dan
|
||||
/// [replayed] hanya ada di response tukar.
|
||||
@freezed
|
||||
abstract class VoucherRedemptionDto with _$VoucherRedemptionDto {
|
||||
const factory VoucherRedemptionDto({
|
||||
@JsonKey(name: 'id') String? id,
|
||||
@JsonKey(name: 'voucher_id') String? voucherId,
|
||||
@JsonKey(name: 'voucher_name') String? voucherName,
|
||||
@JsonKey(name: 'voucher_image_url') String? voucherImageUrl,
|
||||
@JsonKey(name: 'voucher_type') String? voucherType,
|
||||
@JsonKey(name: 'status') String? status,
|
||||
@JsonKey(name: 'face_value') int? faceValue,
|
||||
@JsonKey(name: 'point_cost') int? pointCost,
|
||||
@JsonKey(name: 'code') String? code,
|
||||
@JsonKey(name: 'code_expires_at') String? codeExpiresAt,
|
||||
@JsonKey(name: 'completed_at') String? completedAt,
|
||||
@JsonKey(name: 'created_at') String? createdAt,
|
||||
@JsonKey(name: 'point_balance') int? pointBalance,
|
||||
@JsonKey(name: 'replayed') bool? replayed,
|
||||
}) = _VoucherRedemptionDto;
|
||||
|
||||
factory VoucherRedemptionDto.fromJson(Map<String, dynamic> json) =>
|
||||
_$VoucherRedemptionDtoFromJson(json);
|
||||
|
||||
const VoucherRedemptionDto._();
|
||||
|
||||
VoucherRedemption toDomain() => VoucherRedemption(
|
||||
id: id ?? '',
|
||||
voucherId: voucherId ?? '',
|
||||
voucherName: voucherName ?? '',
|
||||
voucherImageUrl: (voucherImageUrl?.isEmpty ?? true)
|
||||
? null
|
||||
: voucherImageUrl,
|
||||
voucherType: VoucherType.fromApi(voucherType),
|
||||
status: VoucherRedemptionStatus.fromApi(status),
|
||||
faceValue: faceValue ?? 0,
|
||||
pointCost: pointCost ?? 0,
|
||||
code: (code?.isEmpty ?? true) ? null : code,
|
||||
codeExpiresAt: _parseDateTime(codeExpiresAt),
|
||||
completedAt: _parseDateTime(completedAt),
|
||||
createdAt: _parseDateTime(createdAt),
|
||||
);
|
||||
|
||||
VoucherRedeemResult toRedeemResult() => VoucherRedeemResult(
|
||||
redemption: toDomain(),
|
||||
pointBalance: pointBalance ?? 0,
|
||||
replayed: replayed ?? false,
|
||||
);
|
||||
}
|
||||
|
||||
class VoucherRedemptionPageDto {
|
||||
final List<VoucherRedemptionDto> items;
|
||||
final WalletPaginationDto pagination;
|
||||
|
||||
const VoucherRedemptionPageDto({
|
||||
required this.items,
|
||||
required this.pagination,
|
||||
});
|
||||
|
||||
VoucherRedemptionPage toDomain() => VoucherRedemptionPage(
|
||||
items: items.map((e) => e.toDomain()).toList(),
|
||||
page: pagination.page ?? 1,
|
||||
totalPages: pagination.totalPages ?? 1,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:data_channel/data_channel.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
|
||||
import '../../../domain/wallet/wallet.dart';
|
||||
import '../datasources/voucher_remote_data_provider.dart';
|
||||
|
||||
@Injectable(as: IVoucherRepository)
|
||||
class VoucherRepository implements IVoucherRepository {
|
||||
final VoucherRemoteDataProvider _remoteDataProvider;
|
||||
|
||||
final String _logName = 'VoucherRepository';
|
||||
|
||||
VoucherRepository(this._remoteDataProvider);
|
||||
|
||||
@override
|
||||
Future<Either<WalletFailure, List<Voucher>>> getVouchers() => _run(
|
||||
'getVouchers',
|
||||
_remoteDataProvider.fetchVouchers,
|
||||
(dtos) => dtos.map((e) => e.toDomain()).toList(),
|
||||
);
|
||||
|
||||
@override
|
||||
Future<Either<WalletFailure, VoucherRedeemResult>> redeem({
|
||||
required String voucherId,
|
||||
required String pin,
|
||||
required String idempotencyKey,
|
||||
}) => _run(
|
||||
'redeem',
|
||||
() => _remoteDataProvider.redeem(
|
||||
voucherId: voucherId,
|
||||
pin: pin,
|
||||
idempotencyKey: idempotencyKey,
|
||||
),
|
||||
(dto) => dto.toRedeemResult(),
|
||||
);
|
||||
|
||||
@override
|
||||
Future<Either<WalletFailure, VoucherRedemptionPage>> getRedemptions({
|
||||
required int page,
|
||||
int limit = 20,
|
||||
}) => _run(
|
||||
'getRedemptions',
|
||||
() => _remoteDataProvider.fetchRedemptions(page: page, limit: limit),
|
||||
(dto) => dto.toDomain(),
|
||||
);
|
||||
|
||||
Future<Either<WalletFailure, R>> _run<D, R>(
|
||||
String name,
|
||||
Future<DC<WalletFailure, D>> Function() call,
|
||||
R Function(D dto) toDomain,
|
||||
) async {
|
||||
try {
|
||||
final result = await call();
|
||||
if (result.hasError) return left(result.error!);
|
||||
return right(toDomain(result.data as D));
|
||||
} catch (e, s) {
|
||||
log(name, name: _logName, error: e, stackTrace: s);
|
||||
return left(const WalletFailure.unexpectedError());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -50,15 +50,6 @@ class WalletRepository implements IWalletRepository {
|
||||
(dto) => dto.toDomain(),
|
||||
);
|
||||
|
||||
@override
|
||||
Future<Either<WalletFailure, PaymentCode>> createPaymentCode({
|
||||
required String pin,
|
||||
}) => _run(
|
||||
'createPaymentCode',
|
||||
() => _remoteDataProvider.createPaymentCode(pin: pin),
|
||||
(dto) => dto.toDomain(),
|
||||
);
|
||||
|
||||
@override
|
||||
Future<Either<WalletFailure, ExchangePreview>> previewExchange({
|
||||
required int coins,
|
||||
|
||||
@@ -8,12 +8,12 @@ part 'wallet_dtos.g.dart';
|
||||
part 'dto/wallet_summary_dto.dart';
|
||||
part 'dto/wallet_transaction_dto.dart';
|
||||
part 'dto/wallet_expiring_dto.dart';
|
||||
part 'dto/payment_code_dto.dart';
|
||||
part 'dto/exchange_dto.dart';
|
||||
part 'dto/transfer_dto.dart';
|
||||
part 'dto/pin_dto.dart';
|
||||
part 'dto/outlet_dto.dart';
|
||||
part 'dto/customer_order_dto.dart';
|
||||
part 'dto/voucher_dto.dart';
|
||||
|
||||
DateTime? _parseDateTime(String? value) =>
|
||||
value == null ? null : DateTime.tryParse(value);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -112,20 +112,6 @@ _WalletExpiringDto _$WalletExpiringDtoFromJson(Map<String, dynamic> json) =>
|
||||
Map<String, dynamic> _$WalletExpiringDtoToJson(_WalletExpiringDto instance) =>
|
||||
<String, dynamic>{'point': instance.point, 'coin': instance.coin};
|
||||
|
||||
_PaymentCodeDto _$PaymentCodeDtoFromJson(Map<String, dynamic> json) =>
|
||||
_PaymentCodeDto(
|
||||
code: json['code'] as String?,
|
||||
qrPayload: json['qr_payload'] as String?,
|
||||
expiresAt: json['expires_at'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$PaymentCodeDtoToJson(_PaymentCodeDto instance) =>
|
||||
<String, dynamic>{
|
||||
'code': instance.code,
|
||||
'qr_payload': instance.qrPayload,
|
||||
'expires_at': instance.expiresAt,
|
||||
};
|
||||
|
||||
_ExchangePreviewDto _$ExchangePreviewDtoFromJson(Map<String, dynamic> json) =>
|
||||
_ExchangePreviewDto(
|
||||
coinAmount: (json['coin_amount'] as num?)?.toInt(),
|
||||
@@ -253,7 +239,6 @@ _OutletDto _$OutletDtoFromJson(Map<String, dynamic> json) => _OutletDto(
|
||||
id: json['id'] as String?,
|
||||
name: json['name'] as String?,
|
||||
address: json['address'] as String?,
|
||||
acceptsPointPayment: json['accepts_point_payment'] as bool?,
|
||||
earnsPoints: json['earns_points'] as bool?,
|
||||
earnsCoins: json['earns_coins'] as bool?,
|
||||
);
|
||||
@@ -263,7 +248,6 @@ Map<String, dynamic> _$OutletDtoToJson(_OutletDto instance) =>
|
||||
'id': instance.id,
|
||||
'name': instance.name,
|
||||
'address': instance.address,
|
||||
'accepts_point_payment': instance.acceptsPointPayment,
|
||||
'earns_points': instance.earnsPoints,
|
||||
'earns_coins': instance.earnsCoins,
|
||||
};
|
||||
@@ -358,7 +342,6 @@ _CustomerOrderPaymentDto _$CustomerOrderPaymentDtoFromJson(
|
||||
methodType: json['method_type'] as String?,
|
||||
amount: (json['amount'] as num?)?.toInt(),
|
||||
refundAmount: (json['refund_amount'] as num?)?.toInt(),
|
||||
pointsUsed: (json['points_used'] as num?)?.toInt(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$CustomerOrderPaymentDtoToJson(
|
||||
@@ -369,5 +352,69 @@ Map<String, dynamic> _$CustomerOrderPaymentDtoToJson(
|
||||
'method_type': instance.methodType,
|
||||
'amount': instance.amount,
|
||||
'refund_amount': instance.refundAmount,
|
||||
'points_used': instance.pointsUsed,
|
||||
};
|
||||
|
||||
_VoucherDto _$VoucherDtoFromJson(Map<String, dynamic> json) => _VoucherDto(
|
||||
id: json['id'] as String?,
|
||||
name: json['name'] as String?,
|
||||
description: json['description'] as String?,
|
||||
imageUrl: json['image_url'] as String?,
|
||||
voucherType: json['voucher_type'] as String?,
|
||||
faceValue: (json['face_value'] as num?)?.toInt(),
|
||||
pointCost: (json['point_cost'] as num?)?.toInt(),
|
||||
maxPerCustomer: (json['max_per_customer'] as num?)?.toInt(),
|
||||
validUntil: json['valid_until'] as String?,
|
||||
available: (json['available'] as num?)?.toInt(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$VoucherDtoToJson(_VoucherDto instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'name': instance.name,
|
||||
'description': instance.description,
|
||||
'image_url': instance.imageUrl,
|
||||
'voucher_type': instance.voucherType,
|
||||
'face_value': instance.faceValue,
|
||||
'point_cost': instance.pointCost,
|
||||
'max_per_customer': instance.maxPerCustomer,
|
||||
'valid_until': instance.validUntil,
|
||||
'available': instance.available,
|
||||
};
|
||||
|
||||
_VoucherRedemptionDto _$VoucherRedemptionDtoFromJson(
|
||||
Map<String, dynamic> json,
|
||||
) => _VoucherRedemptionDto(
|
||||
id: json['id'] as String?,
|
||||
voucherId: json['voucher_id'] as String?,
|
||||
voucherName: json['voucher_name'] as String?,
|
||||
voucherImageUrl: json['voucher_image_url'] as String?,
|
||||
voucherType: json['voucher_type'] as String?,
|
||||
status: json['status'] as String?,
|
||||
faceValue: (json['face_value'] as num?)?.toInt(),
|
||||
pointCost: (json['point_cost'] as num?)?.toInt(),
|
||||
code: json['code'] as String?,
|
||||
codeExpiresAt: json['code_expires_at'] as String?,
|
||||
completedAt: json['completed_at'] as String?,
|
||||
createdAt: json['created_at'] as String?,
|
||||
pointBalance: (json['point_balance'] as num?)?.toInt(),
|
||||
replayed: json['replayed'] as bool?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$VoucherRedemptionDtoToJson(
|
||||
_VoucherRedemptionDto instance,
|
||||
) => <String, dynamic>{
|
||||
'id': instance.id,
|
||||
'voucher_id': instance.voucherId,
|
||||
'voucher_name': instance.voucherName,
|
||||
'voucher_image_url': instance.voucherImageUrl,
|
||||
'voucher_type': instance.voucherType,
|
||||
'status': instance.status,
|
||||
'face_value': instance.faceValue,
|
||||
'point_cost': instance.pointCost,
|
||||
'code': instance.code,
|
||||
'code_expires_at': instance.codeExpiresAt,
|
||||
'completed_at': instance.completedAt,
|
||||
'created_at': instance.createdAt,
|
||||
'point_balance': instance.pointBalance,
|
||||
'replayed': instance.replayed,
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:dio/dio.dart';
|
||||
|
||||
import '../../common/api/interceptors/unauthorized_interceptor.dart';
|
||||
import '../../domain/wallet/wallet.dart';
|
||||
|
||||
/// Hitung mundur bawaan bila server menjawab `429` tanpa header `Retry-After`.
|
||||
@@ -8,7 +9,7 @@ const int kDefaultOtpRetrySeconds = 60;
|
||||
/// Membaca envelope `{ success, data, errors }`. Mengembalikan `null` bila
|
||||
/// sukses, atau [WalletFailure] yang sesuai bila gagal.
|
||||
WalletFailure? parseWalletFailure(Response response) {
|
||||
if (response.statusCode == 401) return const WalletFailure.unauthorized();
|
||||
if (isTokenRejected(response)) return const WalletFailure.unauthorized();
|
||||
|
||||
final body = response.data;
|
||||
if (body is! Map) return _failureFromStatus(response);
|
||||
|
||||
Reference in New Issue
Block a user