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
+20 -1
View File
@@ -1,5 +1,24 @@
import 'package:flutter/services.dart' show appFlavor;
/// تنظیمات سراسری اپ.
class AppConfig {
/// فروشگاهِ فعال از روی `--flavor` که اپ با آن ساخته شده
/// (`myket` | `bazaar`). اگر بدونِ flavor اجرا شود، `dev`.
static String get store => appFlavor ?? 'dev';
/// کلیدِ عمومیِ RSA مایکت (برای تأییدِ محلیِ افزونه‌ی myket_iap).
static const String myketRsaKey = String.fromEnvironment(
'MYKET_RSA',
defaultValue:
'MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDVCtWUXVZfxULqP3QKlTqsXYkqg5qJYf6KnRSsyk7STB223Ji95IonwHrSrheffrPXi4othieBwl7rDaP0KnvkZ0Le0kr7xHCgTQgYeRuPq15HMm9mrFaXD3HgTF27f1/DcAN8z7eE02ShwAjd0LjYC2u2+T6z8iuQ2oh9LbJPnQIDAQAB',
);
/// کلیدِ عمومیِ RSA کافه‌بازار (از پنلِ توسعه‌دهنده‌ی بازار).
static const String bazaarRsaKey = String.fromEnvironment(
'BAZAAR_RSA',
defaultValue: '',
);
/// آدرس پایه‌ی سرور.
/// - موبایل واقعی روی همان Wi-Fi: IP محلی مک (پیش‌فرض فعلی).
/// - امولاتور اندروید: `--dart-define=BASE_URL=http://10.0.2.2:8080`
@@ -8,7 +27,7 @@ class AppConfig {
/// با تغییر شبکه/IP مک، مقدار dart-define یا همین پیش‌فرض را عوض کنید.
static const String baseUrl = String.fromEnvironment(
'BASE_URL',
defaultValue: 'http://192.168.1.105:8080',
defaultValue: 'https://hakem.approagency.ir',
);
static String get apiUrl => '$baseUrl/api';
+23 -2
View File
@@ -46,7 +46,12 @@ import '../../feature/wallet/domain/repository/wallet_repository.dart';
import '../../feature/wallet/domain/use_cases/claim_daily_usecase.dart';
import '../../feature/wallet/domain/use_cases/get_wallet_usecase.dart';
import '../../feature/wallet/presentation/bloc/wallet_bloc.dart';
import '../config.dart';
import '../network/api_provider_imp.dart';
import '../payment/bazaar_payment_gateway.dart';
import '../payment/dev_payment_gateway.dart';
import '../payment/myket_payment_gateway.dart';
import '../payment/payment_gateway.dart';
import '../storage/token_storage.dart';
final GetIt locator = GetIt.instance;
@@ -94,6 +99,9 @@ Future<void> setupLocator() async {
locator.registerSingleton<SelectCardUseCase>(SelectCardUseCase(locator()));
locator.registerSingleton<PurchaseUseCase>(PurchaseUseCase(locator()));
locator.registerSingleton<AdRewardUseCase>(AdRewardUseCase(locator()));
// درگاهِ پرداخت بر اساسِ flavorِ فعال (مایکت/کافه‌بازار/توسعه).
locator.registerSingleton<PaymentGateway>(_gatewayForStore(AppConfig.store));
locator.registerSingleton<GetProfileUseCase>(GetProfileUseCase(locator()));
locator.registerSingleton<SaveProfileUseCase>(SaveProfileUseCase(locator()));
locator.registerSingleton<GetLeaderboardUseCase>(
@@ -106,8 +114,8 @@ Future<void> setupLocator() async {
locator.registerFactory<AuthBloc>(
() => AuthBloc(locator(), locator(), locator(), locator()));
locator.registerFactory<WalletBloc>(() => WalletBloc(locator(), locator()));
locator.registerFactory<ShopBloc>(() =>
ShopBloc(locator(), locator(), locator(), locator(), locator()));
locator.registerFactory<ShopBloc>(() => ShopBloc(
locator(), locator(), locator(), locator(), locator(), locator()));
locator.registerFactory<ProfileBloc>(
() => ProfileBloc(locator(), locator()));
locator.registerFactory<LeaderboardBloc>(() => LeaderboardBloc(locator()));
@@ -115,3 +123,16 @@ Future<void> setupLocator() async {
locator.registerFactory<TierBloc>(() => TierBloc(locator()));
locator.registerFactory<PrivateInfoBloc>(() => PrivateInfoBloc(locator()));
}
/// درگاهِ پرداختِ متناظر با flavor. buildهای بدونِ flavor از درگاهِ توسعه
/// استفاده می‌کنند (بک‌اند در DevFallback می‌پذیرد).
PaymentGateway _gatewayForStore(String store) {
switch (store) {
case 'myket':
return MyketPaymentGateway();
case 'bazaar':
return BazaarPaymentGateway();
default:
return DevPaymentGateway();
}
}
@@ -0,0 +1,46 @@
import 'package:flutter_poolakey/flutter_poolakey.dart';
import '../config.dart';
import 'payment_gateway.dart';
/// پرداختِ درون‌برنامه‌ایِ کافه‌بازار (Poolakey) — flavorِ `bazaar`.
class BazaarPaymentGateway implements PaymentGateway {
bool _connected = false;
@override
String get store => 'bazaar';
@override
Future<void> connect() async {
if (_connected) return;
await FlutterPoolakey.connect(
AppConfig.bazaarRsaKey,
onDisconnected: () => _connected = false,
);
_connected = true;
}
@override
Future<PurchaseResult> purchase(String productSku) async {
try {
await connect();
final info = await FlutterPoolakey.purchase(
productSku,
payload: 'hakemsho',
);
if (info.purchaseToken.isEmpty) {
return const PurchaseResult.failure();
}
return PurchaseResult.success(
token: info.purchaseToken,
purchaseData: info.originalJson,
signature: info.dataSignature,
// کالای مصرفی باید مصرف شود تا خریدِ مجدد ممکن باشد.
onConfirmed: () => FlutterPoolakey.consume(info.purchaseToken),
);
} catch (_) {
// کنسل‌شدن یا خطای بازار ⇒ شکستِ خرید.
return const PurchaseResult.failure();
}
}
}
+22
View File
@@ -0,0 +1,22 @@
import 'payment_gateway.dart';
/// درگاهِ توسعه: بدونِ اتصال به فروشگاه، یک توکنِ ساختگی می‌سازد. برای اجرای
/// معمولیِ `flutter run` (بدونِ flavor) استفاده می‌شود؛ بک‌اند در حالتِ
/// DevFallback آن را می‌پذیرد.
class DevPaymentGateway implements PaymentGateway {
@override
String get store => 'dev';
@override
Future<void> connect() async {}
@override
Future<PurchaseResult> purchase(String productSku) async {
final stamp = DateTime.now().millisecondsSinceEpoch;
return PurchaseResult.success(
token: 'dev-$productSku-$stamp',
purchaseData: '{"productId":"$productSku","purchaseToken":"dev-$stamp"}',
signature: 'dev',
);
}
}
@@ -0,0 +1,40 @@
import 'package:myket_iap/myket_iap.dart';
import 'package:myket_iap/util/purchase.dart';
import '../config.dart';
import 'payment_gateway.dart';
/// پرداختِ درون‌برنامه‌ایِ مایکت (flavorِ `myket`).
class MyketPaymentGateway implements PaymentGateway {
bool _inited = false;
@override
String get store => 'myket';
@override
Future<void> connect() async {
if (_inited) return;
await MyketIAP.init(rsaKey: AppConfig.myketRsaKey, enableDebugLogging: false);
_inited = true;
}
@override
Future<PurchaseResult> purchase(String productSku) async {
await connect();
final Map<dynamic, dynamic> result =
await MyketIAP.launchPurchaseFlow(sku: productSku, payload: 'hakemsho');
final Purchase? purchase = result[MyketIAP.PURCHASE] as Purchase?;
if (purchase == null || purchase.mToken.isEmpty) {
return const PurchaseResult.failure();
}
return PurchaseResult.success(
token: purchase.mToken,
purchaseData: purchase.mOriginalJson,
signature: purchase.mSignature,
// کالای مصرفی (سکه/بلیت) باید مصرف شود تا کاربر بتواند دوباره بخرد.
onConfirmed: () => MyketIAP.consume(purchase: purchase),
);
}
}
+40
View File
@@ -0,0 +1,40 @@
/// نتیجه‌ی یک خریدِ فروشگاهی، مستقل از اینکه کدام SDK آن را تولید کرده.
///
/// بک‌اند برای تأییدِ آفلاینِ RSA به [purchaseData] (JSONِ خام) و [signature]
/// نیاز دارد؛ [token] هم برای تأییدِ سمت‌سرور/سازگاری فرستاده می‌شود.
class PurchaseResult {
final bool success;
final String token;
final String purchaseData;
final String signature;
/// پاک‌سازیِ لازم پس از تأییدِ بک‌اند (مثلاً مایکت باید خرید را consume کند).
final Future<void> Function()? onConfirmed;
const PurchaseResult.success({
required this.token,
required this.purchaseData,
required this.signature,
this.onConfirmed,
}) : success = true;
const PurchaseResult.failure()
: success = false,
token = '',
purchaseData = '',
signature = '',
onConfirmed = null;
}
/// انتزاعِ SDKِ پرداختِ درون‌برنامه‌ایِ مخصوصِ هر فروشگاه. هر flavor یک پیاده‌سازی
/// ثبت می‌کند؛ بقیه‌ی اپ از SDK بی‌خبر می‌ماند.
abstract class PaymentGateway {
/// شناسه‌ی فروشگاه که به بک‌اند فرستاده می‌شود (`myket` | `bazaar`).
String get store;
/// جریانِ خریدِ [productSku] (همان UUIDِ محصول در پنلِ فروشگاه) را باز می‌کند.
Future<PurchaseResult> purchase(String productSku);
/// اتصالِ اولیه (در صورتِ نیاز). پیش از اولین خرید صدا زده می‌شود.
Future<void> connect() async {}
}
+4
View File
@@ -28,10 +28,14 @@ class PurchaseParams {
final String kind;
final String productId;
final String token;
final String purchaseData;
final String signature;
const PurchaseParams({
required this.store,
required this.kind,
required this.productId,
required this.token,
this.purchaseData = '',
this.signature = '',
});
}