feat: add payment

This commit is contained in:
2026-07-05 16:03:15 +03:30
parent c2356b3242
commit 5017e9a613
30 changed files with 470 additions and 65 deletions
@@ -1,5 +1,6 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../core/payment/payment_gateway.dart';
import '../../../../core/resources/data_state.dart';
import '../../../../core/usecase/use_case.dart';
import '../../domain/use_cases/ad_reward_usecase.dart';
@@ -17,6 +18,7 @@ class ShopBloc extends Bloc<ShopEvent, ShopBlocState> {
final SelectCardUseCase selectCardUseCase;
final PurchaseUseCase purchaseUseCase;
final AdRewardUseCase adRewardUseCase;
final PaymentGateway gateway;
ShopBloc(
this.getShopUseCase,
@@ -24,6 +26,7 @@ class ShopBloc extends Bloc<ShopEvent, ShopBlocState> {
this.selectCardUseCase,
this.purchaseUseCase,
this.adRewardUseCase,
this.gateway,
) : super(ShopBlocState.initial()) {
on<LoadShopEvent>((event, emit) => _load(emit));
@@ -33,15 +36,7 @@ class ShopBloc extends Bloc<ShopEvent, ShopBlocState> {
on<SelectCardEvent>((event, emit) =>
_action(emit, () => selectCardUseCase(event.cardId)));
on<PurchaseEvent>((event, emit) => _action(
emit,
() => purchaseUseCase(PurchaseParams(
store: 'bazaar',
kind: event.kind,
productId: event.productId,
token:
'dev-${event.kind}-${event.productId}-${DateTime.now().millisecondsSinceEpoch}',
))));
on<PurchaseEvent>((event, emit) => _action(emit, () => _purchase(event)));
on<AdRewardEvent>((event, emit) => _action(
emit,
@@ -56,6 +51,42 @@ class ShopBloc extends Bloc<ShopEvent, ShopBlocState> {
));
}
/// جریانِ خرید: ابتدا پرداختِ فروشگاهی (native) اجرا می‌شود، سپس مدرکِ خرید
/// (token/purchaseData/signature) برای اعتبارسنجی به بک‌اند فرستاده می‌شود و
/// در پایان، خرید در فروشگاه consume می‌شود.
Future<DataState<String>> _purchase(PurchaseEvent event) async {
// برای پرداختِ native به SKUِ فروشگاه نیاز است؛ اگر تنظیم نشده باشد از
// شناسه‌ی کاتالوگ استفاده می‌کنیم (حالتِ توسعه/تک‌فروشگاهی).
final sku = event.sku.isNotEmpty ? event.sku : event.productId;
final PurchaseResult result;
try {
result = await gateway.purchase(sku);
} catch (e) {
return DataError<String>('خطا در پرداخت: $e');
}
if (!result.success) {
return DataError<String>('خرید انجام نشد یا لغو شد');
}
final verify = await purchaseUseCase(PurchaseParams(
store: gateway.store,
kind: event.kind,
productId: event.productId,
token: result.token,
purchaseData: result.purchaseData,
signature: result.signature,
));
// پس از تأییدِ بک‌اند، خرید را در فروشگاه نهایی/مصرف می‌کنیم.
if (verify is DataSuccess && result.onConfirmed != null) {
try {
await result.onConfirmed!();
} catch (_) {/* consume ناموفق حیاتی نیست */}
}
return verify;
}
Future<void> _load(Emitter<ShopBlocState> emit) async {
emit(state.copyWith(loadStatus: ShopLoading()));
final res = await getShopUseCase(const NoParams());
@@ -14,8 +14,9 @@ class SelectCardEvent extends ShopEvent {
class PurchaseEvent extends ShopEvent {
final String kind;
final String productId;
PurchaseEvent(this.kind, this.productId);
final String productId; // شناسه‌ی کاتالوگ (برای اعتباردهی در بک‌اند)
final String sku; // شناسه‌ی محصول در پنلِ فروشگاه (برای پرداختِ native)
PurchaseEvent(this.kind, this.productId, {this.sku = ''});
}
class AdRewardEvent extends ShopEvent {}
@@ -211,7 +211,7 @@ class _CoinsTab extends StatelessWidget {
label: '${p.priceToman} تومان',
onTap:
() =>
context.read<ShopBloc>().add(PurchaseEvent('coin', p.id)),
context.read<ShopBloc>().add(PurchaseEvent('coin', p.id, sku: p.sku)),
),
),
],
@@ -237,7 +237,7 @@ class _TicketsTab extends StatelessWidget {
label: '${p.priceToman} تومان',
onTap:
() => context.read<ShopBloc>().add(
PurchaseEvent('ticket', p.id),
PurchaseEvent('ticket', p.id, sku: p.sku),
),
),
),
@@ -264,7 +264,7 @@ class _BoostersTab extends StatelessWidget {
label: '${b.priceToman} تومان',
onTap:
() => context.read<ShopBloc>().add(
PurchaseEvent('booster', b.id),
PurchaseEvent('booster', b.id, sku: b.sku),
),
),
),
@@ -292,7 +292,7 @@ class _VipTab extends StatelessWidget {
label: '${p.priceToman} تومان',
onTap:
() =>
context.read<ShopBloc>().add(PurchaseEvent('vip', p.id)),
context.read<ShopBloc>().add(PurchaseEvent('vip', p.id, sku: p.sku)),
),
),
],
@@ -110,7 +110,7 @@ class VipScreen extends StatelessWidget {
busy: state.busy,
onBuy: () => context
.read<ShopBloc>()
.add(PurchaseEvent('vip', p.id)),
.add(PurchaseEvent('vip', p.id, sku: p.sku)),
),
),
if (packages.isEmpty)