feat: Phase 1 — remove Stripe, switch to e-Transfer

- Drop stripe_payment + stripe_sdk deps; delete stripe pay pages/widgets
- New ETransferPay widget: shows payment@wisetronic.com + R#<orderId> remark
- goPayment stripe/native branches now route to e-Transfer
- pay_now widgets: strip saved-cards section & dead nativePay logic
- my_cards pages rewritten as e-Transfer info (route kept)
- models/utils/constants cleaned of stripe; env constraint -> Dart 3

See UPGRADE_NOTES.md. (Null-safety migration is Phase 3; deps upgrade Phase 2.)
This commit is contained in:
2026-07-24 02:47:13 +08:00
parent f8a90ad305
commit 7ffcc848d0
17 changed files with 371 additions and 1548 deletions

40
UPGRADE_NOTES.md Normal file
View File

@@ -0,0 +1,40 @@
# flutter_wisetronic 升级日志
## 目标
- 升级到 Dart 3 强制空安全(目标 SDK本机 Flutter 3.29.3 / Dart 3.7.2,不升级全局)
- 移除 Stripe 收款,改为页面展示 e-transfer email 让客户自行 e-transfer 付款
- 第一阶段能编译、Web 能跑起来(暂不美化)
## 关键决策
- 目标版本Flutter 3.29.3 / Dart 3.7.2(钉定本机,不动全局)
- e-transfer 邮箱:`payment@wisetronic.com`
- 提醒客户在 e-transfer remark 里写 `R#<receipt.id>` 以便自动化处理订单
- 搜索框:自写轻量 Web 搜索组件替换 flappy_search_bar
- Dart 3 已移除 `dart migrate`,空安全迁移用手工 + IDE 修复
## 阶段
- [x] 阶段 0基线 & 锁版本
- [x] 阶段 1去掉 Stripe
- [ ] 阶段 2依赖替换与升级
- [ ] 阶段 3空安全迁移
- [ ] 阶段 4Web 跑起来
---
## 改动记录
### 阶段 0
- 创建 UPGRADE_NOTES.md
- pubspec.yaml: environment sdk `>=2.7.0 <3.0.0``>=3.0.0 <4.0.0`flutter `>=3.29.0`
### 阶段 1 — 去掉 Stripe已完成
- 删除: stripe_pay_web.dart, stripe_pay.dart, desktop/mobile_stripe_pay_web.dart, stripe_payment_method.dart
- pubspec: 移除 stripe_payment + stripe_sdk(git)
- user.dart: 移除 stripePaymentMethods 字段与解析
- utils.dart: 移除 createOrUpdateStripePaymentMethod / stripePaymentIntent / stripeChargedSuccess
- util_web.dart / util_io.dart: goPayment 的 stripe/native 分支 → 跳转 ETransferPay
- desktop/mobile_pay_now.dart: 移除“已有卡片”区块与 nativePay 死逻辑,简化列表索引
- desktop/mobile_my_cards.dart: 重写为 e-Transfer 说明页(保留路由入口)
- constants.dart: 新增 ETRANSFER_EMAIL(payment@wisetronic.com) 与 REMARK_PREFIX(R#)
- 新增 lib/widgets/general/e_transfer_pay.dart: 展示邮箱 + 提醒备注写 R#<orderId>
- 保留: PAYMENT_METHOD_CODE_STRIPE='stripe'(API 仍返回该 code路由到 e-Transfer)payment_platform.dart 的 'stripe' 显示名(后续美化时调整)

View File

@@ -49,13 +49,15 @@ class Constants {
static const String PAYMENT_METHOD_CODE_OTT_ALIPAY = 'ALIPAY';
static const String PAYMENT_METHOD_CODE_OTT_WECHATPAY = 'WECHATPAY';
static const String PAYMENT_METHOD_CODE_PAYPAL = 'paypal';
static const String PAYMENT_METHOD_CODE_STRIPE = 'stripe';
static const String PAYMENT_METHOD_CODE_STRIPE = 'stripe'; // API still returns 'stripe' code; now routed to e-Transfer
static const String PAYMENT_METHOD_CODE_POD = 'payondeliverypickup';
static const String PAYMENT_METHOD_NATIVE_PAY = 'native_pay';
static const String STRIPE_STATUS_REQUIRES_CONFIRMATION = 'requires_confirmation';
static const String STRIPE_CLIENT_SECRET = 'client_secret';
static const String STRIPE_STATUS_SUCCEDED = 'succeeded';
// e-Transfer (replaces Stripe)
static const String ETRANSFER_EMAIL = 'payment@wisetronic.com';
static const String ETRANSFER_QA = 'wisetronic'; // security question answer placeholder, if needed
// Remark template for auto-processing: R#<receipt id> (= order id)
static const String ETRANSFER_REMARK_PREFIX = 'R#';
static const int STATUS_PENDING = 0;
static const int STATUS_ACCEPT = 1;

View File

@@ -1,46 +0,0 @@
import 'dart:convert';
class StripePaymentMethod {
int id;
String customerId;
String paymentMethodId;
String paymentMethodType;
String cardBrand;
String cardCountry;
int cardExpMonth;
int cardExpYear;
String cardFunding;
String cardLast4;
StripePaymentMethod.fromJson(Map<String, dynamic> json) :
id = json['id'],
customerId = json['customer_id'],
paymentMethodId = json['payment_method_id'],
paymentMethodType = json['payment_method_type'],
cardBrand = json['card_brand'],
cardCountry = json['card_country'],
cardExpMonth = json['card_exp_month'],
cardExpYear = json['card_exp_year'],
cardFunding = json['card_funding'],
cardLast4 = json['card_last4'];
Map<String, dynamic> toJson() => {
'id': id,
'customer_id': customerId,
'payment_method_id': paymentMethodId,
'payment_method_type': paymentMethodType,
'card_brand': cardBrand,
'card_country': cardCountry,
'card_exp_month': cardExpMonth,
'card_exp_year': cardExpYear,
'card_funding': cardFunding,
'card_last4': cardLast4,
};
@override
String toString() {
return json.encode(this);
}
}

View File

@@ -1,8 +1,6 @@
import 'dart:convert';
import 'stripe_payment_method.dart';
class User {
int id;
String username;
@@ -20,7 +18,6 @@ class User {
double pointsToCreditsConversionRate;
String contactNumber;
String flutterMiniStoreToken;
List<StripePaymentMethod> stripePaymentMethods;
User.fromJson(Map<String, dynamic> json)
: id = json['id'],
@@ -38,8 +35,7 @@ class User {
orderNum = json['order_num'],
pointsToCreditsConversionRate = double.parse(json['points_to_credits_conversion_rate'].toString()),
contactNumber = json['contact_number'],
flutterMiniStoreToken = json['flutter_ministore_token'],
stripePaymentMethods = (json['stripe_payment_methods'] as List).map((e) => StripePaymentMethod.fromJson(e)).toList();
flutterMiniStoreToken = json['flutter_ministore_token'];
Map<String, dynamic> toJson() => {
'id': id,
@@ -58,7 +54,6 @@ class User {
'points_to_credits_conversion_rate': pointsToCreditsConversionRate,
'contact_number': contactNumber,
'flutter_ministore_token': flutterMiniStoreToken,
'stripe_payment_methods': stripePaymentMethods,
};
@override

View File

@@ -1,34 +0,0 @@
import 'package:flutter/material.dart';
import 'package:responsive_builder/responsive_builder.dart';
import '../models/order.dart';
import '../models/payment_platform.dart';
import '../models/stripe_payment_method.dart';
import '../store/actions.dart';
import '../store/store.dart';
import '../widgets/desktop/desktop_stripe_pay_web.dart';
import '../widgets/mobile/mobile_stripe_pay_web.dart';
class StripePayWeb extends StatelessWidget {
final Order order;
final PaymentPlatform paymentPlatform;
final StripePaymentMethod stripePaymentMethod;
const StripePayWeb(this.order, this.paymentPlatform, {this.stripePaymentMethod});
@override
Widget build(BuildContext context) {
store.dispatch(UpdateContext(context));
return ResponsiveBuilder(
builder: (context, sizingInformation) =>
ScreenTypeLayout(
mobile: MobileStripePayWeb(order, paymentPlatform, stripePaymentMethod: stripePaymentMethod,),
tablet: DesktopStripePayWeb(order, paymentPlatform, stripePaymentMethod: stripePaymentMethod,),
desktop: DesktopStripePayWeb(order, paymentPlatform, stripePaymentMethod: stripePaymentMethod,),
),
);
}
}

View File

@@ -15,7 +15,6 @@ import 'package:hive/hive.dart';
import 'package:image_picker/image_picker.dart';
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
import 'package:stripe_payment/stripe_payment.dart';
import '../constants.dart';
import '../events/eventbus.dart';
@@ -24,12 +23,11 @@ import '../generated/l10n.dart';
import '../models/comment.dart';
import '../models/order.dart';
import '../models/payment_platform.dart';
import '../models/stripe_payment_method.dart';
import '../models/user.dart';
import '../routes.dart';
import '../store/actions.dart';
import '../store/store.dart';
import '../widgets/general/stripe_pay.dart';
import '../widgets/general/e_transfer_pay.dart';
import 'http_util.dart';
import 'utils.dart';
@@ -507,62 +505,14 @@ class Util {
}
Widget getNativePay(BuildContext context, Order order, PaymentPlatform paymentPlatform) {
return GestureDetector(
child: Container(
padding: EdgeInsets.only(top: 16.0, left: 16.0, right: 16.0, bottom: 16.0),
child: Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Container(
margin: EdgeInsets.only(right: 6.0),
child: Text(
Platform.isAndroid ? S.of(context).pay_with : S.of(context).pay_with,
style: TextStyle(
fontSize: 18.0,
fontWeight: FontWeight.bold,
),
),
),
Container(
padding: EdgeInsets.only(right: 10.0),
child: Platform.isAndroid ? Image.asset(
'assets/images/google_pay.png',
height: 22.0,
fit: BoxFit.fitHeight,
) : Image.asset(
'assets/images/apple_pay.png',
height: 22.0,
fit: BoxFit.fitHeight,
),
),
],
),
),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
width: 10,
color: Colors.black26,
),
),
),
),
onTap: () {
print(paymentPlatform);
PaymentPlatform newPaymentPlatform = PaymentPlatform.fromJson(json.decode(json.encode(paymentPlatform)));
newPaymentPlatform.code = Constants.PAYMENT_METHOD_NATIVE_PAY;
goPayment(context, order, newPaymentPlatform);
},
);
// Native (Apple/Google) pay removed with Stripe. Web uses e-Transfer.
return SizedBox.shrink();
}
static void goPayment(BuildContext context, Order order,
PaymentPlatform paymentPlatform, {
bool googlePay=false,
bool applePay=false,
StripePaymentMethod stripePaymentMethod,
}) {
switch(paymentPlatform.code) {
case Constants.PAYMENT_METHOD_CODE_SQUARE:
@@ -584,102 +534,23 @@ class Util {
break;
case Constants.PAYMENT_METHOD_NATIVE_PAY:
StripePayment.setOptions(
StripeOptions(publishableKey: paymentPlatform.publishableKey,
merchantId: paymentPlatform.merchantId,
androidPayMode: paymentPlatform.publishableKey.contains('_test_')
? 'test'
: 'production'
)
);
StripePayment.paymentRequestWithNativePay(
androidPayOptions: AndroidPayPaymentRequest(
totalPrice: order.totalPrice.toStringAsFixed(2),
currencyCode: order.businessInfo.currency,
),
applePayOptions: ApplePayPaymentOptions(
currencyCode: order.businessInfo.currency,
countryCode: order.businessInfo.address.country,
items: [
ApplePayItem(
label: order.businessInfo.name,
amount: order.totalPrice.toStringAsFixed(2),
),
],
),
).then((token) {
print('return native token: ${token.tokenId}');
processNativePay(context, token, order);
}).catchError((error) {
print('native pay error: $error');
});
// Native (Apple/Google) pay removed with Stripe; route to e-Transfer.
Navigator.pushReplacement(context, MaterialPageRoute(
builder: (BuildContext context) {
return ETransferPay(order: order);
}
));
break;
case Constants.PAYMENT_METHOD_CODE_STRIPE:
Navigator.pushReplacement(context, MaterialPageRoute(
builder: (BuildContext context) {
return StripePay(order, paymentPlatform, stripePaymentMethod: stripePaymentMethod,);
return ETransferPay(order: order);
}
));
break;
}
}
static void processNativePay(BuildContext context, Token token, Order order) async {
PaymentMethod paymentMethod = await StripePayment.createPaymentMethod(
PaymentMethodRequest(
card: CreditCard(
token: token.tokenId,
),
),
);
if (paymentMethod != null) {
Utils.stripePaymentIntent(order, null, paymentMethod.id, paymentMethod.type, (response) {
if (response.data['status'] == Constants.STRIPE_STATUS_REQUIRES_CONFIRMATION) {
StripePayment.confirmPaymentIntent(
PaymentIntent(
clientSecret: response.data[Constants.STRIPE_CLIENT_SECRET],
paymentMethodId: response.data['payment_method'],
),
).then((paymentIntentResult) {
if (paymentIntentResult.status == Constants.STRIPE_STATUS_SUCCEDED) {
Utils.stripeChargedSuccess(order,
paymentMethod.id,
paymentIntentResult.paymentIntentId,
(response) {
StripePayment.completeNativePayRequest().then((_) {
eventBus.fire(OnOrderUpdated());
Routes.router.navigateTo(context, '/orderdetail/${order.id}', replace: true);
}).catchError((error) {
Utils.showMessageDialog(context, error, onOk: () {
Navigator.of(context).pop();
});
});
},
(error) {
Utils.showMessageDialog(context, error, onOk: () {
Navigator.of(context).pop();
});
}
);
} else {
Utils.showMessageDialog(context, Exception('Unknown error'));
}
}).catchError((error) {
Utils.showMessageDialog(context, error, onOk: () {
Navigator.of(context).pop();
});
});
}
}, (error) {
Utils.showMessageDialog(context, error, onOk: () {
Navigator.of(context).pop();
});
});
} else {
}
}
static Future<Uint8List> getBytesFromAsset(String path, int width) async {
ByteData data = await rootBundle.load(path);
ui.Codec codec = await ui.instantiateImageCodec(data.buffer.asUint8List(), targetWidth: width);

View File

@@ -16,11 +16,10 @@ import '../generated/l10n.dart';
import '../models/comment.dart' as mComment;
import '../models/order.dart';
import '../models/payment_platform.dart';
import '../models/stripe_payment_method.dart';
import '../models/user.dart';
import '../store/actions.dart';
import '../store/store.dart';
import '../pages/stripe_pay_web.dart';
import '../widgets/general/e_transfer_pay.dart';
import 'http_util.dart';
import 'utils.dart';
@@ -252,7 +251,6 @@ class Util {
PaymentPlatform paymentPlatform, {
bool googlePay=false,
bool applePay=false,
StripePaymentMethod stripePaymentMethod,
}) {
switch(paymentPlatform.code) {
case Constants.PAYMENT_METHOD_CODE_SQUARE:
@@ -276,7 +274,7 @@ class Util {
case Constants.PAYMENT_METHOD_CODE_STRIPE:
Navigator.pushReplacement(context, MaterialPageRoute(
builder: (BuildContext context) {
return StripePayWeb(order, paymentPlatform, stripePaymentMethod: stripePaymentMethod,);
return ETransferPay(order: order);
}
));
break;

View File

@@ -291,39 +291,6 @@ class Utils {
return amount.toStringAsFixed(decimalPlace);
}
static void createOrUpdateStripePaymentMethod(
String paymentMethodId,
String cardBrand,
String paymentMethodType,
String cardCountry,
int cardExpMonth,
int cardExpYear,
String cardFunding,
String cardLast4,
) {
HttpUtil.httpPost(
'v1/create-update-stripe-payment-method',
(response) {
if (response.statusCode == 200) {
print(
'create or update customer stripe payment method success. ${response.data}');
}
},
body: {
'payment_method_id': paymentMethodId,
'card_brand': cardBrand,
'payment_method_type': paymentMethodType,
'card_country': cardCountry,
'card_exp_month': cardExpMonth,
'card_exp_year': cardExpYear,
'card_funding': cardFunding,
'card_last4': cardLast4,
},
isFormData: true,
).catchError((error) {
print('Error: ${error}');
});
}
static showSubmitDialog(BuildContext context) {
showDialog(
@@ -900,48 +867,7 @@ class Utils {
}
}
static void stripePaymentIntent(Order order, String customerId, String paymentMethodId, String cardType,
OnSuccess onSuccess, OnError onError,
{
String cardBrand,
String cardCountry,
int cardExpMonth,
int cardExpYear,
String cardFunding,
String cardLast4,
}) {
HttpUtil.httpPost('v1/stripe-payment-intent',
onSuccess,
body: {
'order_id': order.id,
'pay_amount': order.totalPrice,
'customer_id': customerId == null ? '' : customerId,
'payment_method_id': paymentMethodId,
'payment_method_type': cardType,
'card_brand': cardBrand,
'card_country': cardCountry,
'card_exp_month': cardExpMonth,
'card_exp_year': cardExpYear,
'card_funding': cardFunding,
'card_last4': cardLast4,
},
isFormData: true,
).catchError(onError);
}
static void stripeChargedSuccess(Order order, String paymentMethodId,
String paymentIntentId, OnSuccess onSuccess, OnError onError) {
HttpUtil.httpPost('v1/stripe-charged-success',
onSuccess,
isFormData: true,
body: {
'order_id': order.id,
'payment_method_id': paymentMethodId,
'payment_intent_id': paymentIntentId,
}
).catchError(onError);
}
static int getProductLineInOrder(CartInfo cartInfo) {
int qty = 0;

View File

@@ -1,49 +1,24 @@
import 'package:flutter/material.dart';
import '../../constants.dart';
import '../../events/eventbus.dart';
import '../../events/events.dart';
import '../../generated/l10n.dart';
import '../../models/stripe_payment_method.dart';
import '../../models/user.dart';
import '../../store/actions.dart';
import '../../store/store.dart';
import '../../utils/http_util.dart';
import '../../utils/utils.dart';
import '../../widgets/general/bottom_nav.dart';
import '../../widgets/general/breadcrumbs.dart';
import '../../widgets/general/navigationbar.dart';
class DesktopMyCards extends StatefulWidget {
final Key key;
const DesktopMyCards({this.key});
@override
State<StatefulWidget> createState() {
return MyCardsState();
}
}
class MyCardsState extends State<DesktopMyCards> {
User _user;
bool isSubmitting;
double sideSpace = 0;
double mainSpace = 1200;
/// “My Cards” 页已停止保存银行卡Stripe 移除)。改为展示 e-Transfer 说明。
class DesktopMyCards extends StatelessWidget {
const DesktopMyCards({Key? key});
@override
Widget build(BuildContext context) {
store.dispatch(UpdateContext(context));
if (MediaQuery.of(context).size.width <= 1200) {
mainSpace = MediaQuery.of(context).size.width;
sideSpace = 0;
} else {
mainSpace = 1200;
double sideSpace = 0;
if (MediaQuery.of(context).size.width > 1200) {
sideSpace = (MediaQuery.of(context).size.width - 1200) / 2;
}
@@ -58,153 +33,62 @@ class MyCardsState extends State<DesktopMyCards> {
),
body: Row(
children: [
Container(
width: sideSpace,
),
Container(width: sideSpace),
Expanded(
child: ListView.builder(
itemCount: _user.stripePaymentMethods.length,
itemBuilder: (BuildContext context, int position) {
StripePaymentMethod paymentMethod = _user.stripePaymentMethods[position];
return cardWidget(paymentMethod);
}
),
),
Container(
width: sideSpace,
),
],
),
bottomNavigationBar: BottomNav(),
);
}
Widget cardWidget(StripePaymentMethod paymentMethod) {
return Container(
padding: EdgeInsets.only(
top: 16.0, left: 16.0, right: 16.0, bottom: 16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
paymentMethod.cardBrand == 'visa' ?
Image.asset(
'assets/images/visa.png',
width: 50.0,
height: 50.0,
fit: BoxFit.fill,
) : (paymentMethod.cardBrand == 'mastercard' ?
Image.asset(
'assets/images/master.png',
width: 50.0,
height: 50.0,
fit: BoxFit.fill,
) : Icon(
Icons.credit_card, size: 50.0, color: Colors.black38,)),
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
child: ListView(
children: [
Container(
margin: EdgeInsets.only(left: 20.0, right: 10.0),
child: Text(
'**** ${paymentMethod.cardLast4}',
style: TextStyle(
fontSize: 20.0,
),
margin: const EdgeInsets.all(24),
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: Colors.lightBlueAccent.withOpacity(0.08),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.black12),
),
),
Container(
margin: EdgeInsets.only(left: 20.0, right: 10.0),
child: Text(
S.of(context).expire_token(paymentMethod.cardExpMonth, paymentMethod.cardExpYear),
style: TextStyle(
fontSize: 14.0,
color: Colors.black26,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Row(
children: [
Icon(Icons.info_outline, color: Colors.lightBlueAccent),
SizedBox(width: 8),
Text(
'Saved cards are no longer used',
style: TextStyle(
fontSize: 18, fontWeight: FontWeight.bold),
),
],
),
const SizedBox(height: 12),
const Text(
'We have switched to Interac e-Transfer for payments. '
'You no longer need to save a credit card.',
),
const SizedBox(height: 16),
const Text('Send your e-Transfer to:'),
const SizedBox(height: 4),
SelectableText(
Constants.ETRANSFER_EMAIL,
style: const TextStyle(
fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 12),
const Text(
'Remember to put your order code '
'(e.g. R#<order id>) in the e-Transfer message so we '
'can process your order automatically.',
style: TextStyle(color: Colors.black54),
),
],
),
),
],
),
),
IconButton(
icon: Icon(Icons.clear, color: Colors.black26,),
onPressed: () {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text(S.of(context).warning),
content: Text(
S.of(context).are_you_sure_to_remove_the_card,
),
actions: <Widget>[
TextButton(
child: Text(S.of(context).cancel),
onPressed: () {
Navigator.of(context).pop();
},
style: TextButton.styleFrom(
primary: Theme.of(context).primaryColor,
),
),
TextButton(
child: Text(S.of(context).yes_i_am_sure),
onPressed: () {
Navigator.of(context).pop();
_removeCard(paymentMethod);
},
)
],
);
}
);
},
),
Container(width: sideSpace),
],
),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
width: 0.5,
color: Colors.black12,
),
),
),
bottomNavigationBar: BottomNav(),
);
}
_removeCard(StripePaymentMethod paymentMethod) {
HttpUtil.httpDelete('v1/stripe-card/${paymentMethod.id}', (response) {
_user = User.fromJson(response.data);
store.dispatch(UpdateCurrentUser(_user));
eventBus.fire(OnCurrentUserUpdated());
if (mounted) {
Navigator.of(context).pop();
setState(() {
isSubmitting = false;
});
}
}).catchError((error) {
if (isSubmitting) {
Navigator.of(context).pop();
isSubmitting = false;
}
Utils.showMessageDialog(context, error, onOk: () {
Navigator.of(context).pop();
Navigator.of(context).pop();
});
});
isSubmitting = true;
Utils.showSubmitDialog(context);
}
@override
void initState() {
super.initState();
setState(() {
isSubmitting = false;
_user = store.state.user;
});
}
}

View File

@@ -11,7 +11,6 @@ import '../../events/events.dart';
import '../../generated/l10n.dart';
import '../../models/order.dart';
import '../../models/payment_platform.dart';
import '../../models/stripe_payment_method.dart';
import '../../models/user.dart';
import '../../routes.dart';
import '../../store/actions.dart';
@@ -19,7 +18,6 @@ import '../../store/store.dart';
import '../../utils/http_util.dart';
import '../../utils/util_web.dart' if (dart.library.io) '../../utils/util_io.dart';
import '../../utils/utils.dart';
import '../../widgets/general/payment_verification_code_dialog.dart';
class DesktopPayNow extends StatefulWidget {
final Key key;
@@ -38,8 +36,6 @@ class DesktopPayNowState extends State<DesktopPayNow> {
List<PaymentPlatform> paymentPlatforms;
User _user;
bool nativePay;
double sideSpace = 0;
double mainSpace = 1200;
@@ -69,7 +65,7 @@ class DesktopPayNowState extends State<DesktopPayNow> {
BuildContext mainContext = context;
ListView listView = ListView.builder(
itemCount: nativePay ? paymentPlatforms.length + 3 : paymentPlatforms.length + 2,
itemCount: paymentPlatforms.length + 1,
itemBuilder: (BuildContext context, int position) {
if (position == 0) {
return Column(
@@ -169,135 +165,7 @@ class DesktopPayNowState extends State<DesktopPayNow> {
);
}
PaymentPlatform paymentPlatform;
if (position == 1) {
if (_user.stripePaymentMethods.length > 0) {
paymentPlatform = paymentPlatforms[position - 1];
Column column = Column(
children: <Widget>[],
);
column.children.add(
Container(
padding: EdgeInsets.all(16.0),
width: double.infinity,
child: Text(
S.of(context).pay_with_existing_cards,
textAlign: TextAlign.left,
),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
width: 1,
color: Colors.black26,
),
),
),
),
);
for (StripePaymentMethod stripePaymentMethod in _user.stripePaymentMethods) {
column.children.add(
GestureDetector(
child: Container(
padding: EdgeInsets.only(
top: 16.0, left: 16.0, right: 16.0, bottom: 16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
stripePaymentMethod.cardBrand == 'visa' ?
Image.asset(
'assets/images/visa.png',
width: 50.0,
height: 50.0,
fit: BoxFit.fill,
) : (stripePaymentMethod.cardBrand == 'mastercard' ?
Image.asset(
'assets/images/master.png',
width: 50.0,
height: 50.0,
fit: BoxFit.fill,
) : Icon(
Icons.credit_card, size: 50.0, color: Colors.black38,)),
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Container(
margin: EdgeInsets.only(left: 20.0, right: 10.0),
child: Text(
'**** ${stripePaymentMethod.cardLast4}',
style: TextStyle(
fontSize: 20.0,
),
),
),
Container(
margin: EdgeInsets.only(left: 20.0, right: 10.0),
child: Text(
S.of(context).expire_token(stripePaymentMethod.cardExpMonth, stripePaymentMethod.cardExpYear),
style: TextStyle(
fontSize: 14.0,
color: Colors.black26,
),
),
),
],
),
),
Icon(
Icons.arrow_forward_ios,
color: Colors.black26,
),
],
),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
width: 0.5,
color: Colors.black12,
),
),
),
),
onTap: () {
showDialog(
context: mainContext,
builder: (BuildContext context) {
return PaymentVerificationCodeDialog(_user, () {
Util.goPayment(context, order, paymentPlatform,
stripePaymentMethod: stripePaymentMethod);
}, () {
});
},
);
},
),
);
}
column.children.add(
Container(
width: double.infinity,
child: SizedBox.shrink(),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
width: 10,
color: Colors.black26,
),
),
),
)
);
return column;
} else {
return SizedBox.shrink();
}
}
if (position == 2 && nativePay) {
paymentPlatform = paymentPlatforms[position - 2];
return Util().getNativePay(mainContext, order, paymentPlatform);
}
paymentPlatform = nativePay ? paymentPlatforms[position - 3] : paymentPlatforms[position - 2];
paymentPlatform = paymentPlatforms[position - 1];
return GestureDetector(
child: Container(
padding: EdgeInsets.only(top: 16.0, left: 16.0, right: 16.0, bottom: 16.0),
@@ -371,14 +239,6 @@ class DesktopPayNowState extends State<DesktopPayNow> {
).then((data) {
paymentPlatforms = (data['payment_platforms'] as List).map((e) =>
PaymentPlatform.fromJson(e)).toList();
PaymentPlatform pp = paymentPlatforms[0];
if (Constants.ENABLE_NATIVE_PAY && pp.publishableKey != null
&& pp.publishableKey.isNotEmpty && pp.merchantId != null
&& pp.merchantId.isNotEmpty) {
nativePay = true;
} else {
nativePay = false;
}
_user = User.fromJson(data['contact']);
store.dispatch(UpdateCurrentUser(_user));
eventBus.fire(OnCurrentUserUpdated());

View File

@@ -1,250 +0,0 @@
import 'package:flutter/material.dart';
import 'package:stripe_sdk/stripe_sdk.dart';
import 'package:stripe_sdk/stripe_sdk_ui.dart';
import '../../constants.dart';
import '../../events/eventbus.dart';
import '../../events/events.dart';
import '../../generated/l10n.dart';
import '../../models/order.dart';
import '../../models/payment_platform.dart';
import '../../models/stripe_payment_method.dart';
import '../../routes.dart';
import '../../store/actions.dart';
import '../../store/store.dart';
import '../../utils/utils.dart';
import '../../widgets/general/breadcrumbs.dart';
import '../../widgets/general/navigationbar.dart';
class DesktopStripePayWeb extends StatefulWidget {
final Key key;
final Order order;
final PaymentPlatform paymentPlatform;
final StripePaymentMethod stripePaymentMethod;
const DesktopStripePayWeb(this.order, this.paymentPlatform, {this.key, this.stripePaymentMethod});
@override
State<StatefulWidget> createState() {
return DesktopStripePayWebState();
}
}
class DesktopStripePayWebState extends State<DesktopStripePayWeb> {
GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey();
final formKey = GlobalKey<FormState>();
final card = StripeCard();
CardForm form;
bool isSubmitting;
double sideSpace = 0;
double mainSpace = 1200;
@override
Widget build(BuildContext context) {
store.dispatch(UpdateContext(context));
if (MediaQuery.of(context).size.width <= 1200) {
mainSpace = MediaQuery.of(context).size.width;
sideSpace = 0;
} else {
mainSpace = 1200;
sideSpace = (MediaQuery.of(context).size.width - 1200) / 2;
}
Widget body = Center(
child: Icon(
Icons.credit_card,
size: 40.0,
color: Colors.black26,
),
);
if (widget.stripePaymentMethod == null) {
form = CardForm(card: card, formKey: formKey, displayPostalCode: false,);
body = ListView(
children: <Widget>[
form,
Container(
padding: EdgeInsets.only(top: 20.0, bottom: 20.0, right: 16.0),
alignment: Alignment.centerRight,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
primary: Theme.of(context).primaryColor,
),
child: Text(
S.of(context).submit,
style: TextStyle(
color: Colors.white,
),
),
onPressed: () {
if (formKey.currentState.validate()) {
formKey.currentState.save();
_paymentRequestWithCard(context);
} else {
ScaffoldMessenger.of(context).showSnackBar(
messageSnackBar(
context, S.of(context).this_credit_card_is_invalid
)
);
}
},
),
),
],
);
}
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
if (widget.stripePaymentMethod != null) {
_paymentWithPaymentMethod(context);
}
});
return Scaffold(
key: _scaffoldKey,
appBar: MiniNavigationBar(
title: S.of(context).blog,
back: true,
breadCrumbs: [
BreadCrumb(S.of(context).add_credit_card, null),
],
breadCrumbHeight: Constants.BREADCRUMB_HEIGHT,
),
body: Row(
children: [
Container(width: sideSpace,),
Expanded(child: body,),
Container(width: sideSpace,),
],
),
);
}
@override
void initState() {
super.initState();
isSubmitting = false;
StripeApi.init(widget.paymentPlatform.publishableKey);
}
SnackBar messageSnackBar(BuildContext context, String message) {
Column column = Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[Text(
message,
style: TextStyle(
color: Colors.white,
),
)],
);
return SnackBar(
content: Container(
height: 45.0,
child: column,
),
action: SnackBarAction(
label: S.of(context).ok,
onPressed: () {
ScaffoldMessenger.of(context).hideCurrentSnackBar();
},
),
);
}
_paymentWithPaymentMethod(BuildContext context) async {
Utils.stripePaymentIntent(widget.order, widget.stripePaymentMethod.customerId,
widget.stripePaymentMethod.paymentMethodId,
widget.stripePaymentMethod.paymentMethodType, (response) async {
if (response.data['status'] == Constants.STRIPE_STATUS_REQUIRES_CONFIRMATION) {
await StripeApi.instance.confirmPaymentIntent(
response.data[Constants.STRIPE_CLIENT_SECRET],
data: {
'payment_method': response.data['payment_method'],
},
).then((result2) {
if (result2['status'] == Constants.STRIPE_STATUS_SUCCEDED) {
Utils.stripeChargedSuccess(widget.order,
widget.stripePaymentMethod.paymentMethodId,
result2['id'], (response) {
if (isSubmitting) {
Navigator.of(context).pop();
}
eventBus.fire(OnOrderUpdated());
Routes.router.navigateTo(context, '/orderdetail/${widget
.order.id}', replace: true);
},
(showErrorDialog)
);
} else {
showErrorDialog(Exception('Unknown error'));
}
}).catchError(showErrorDialog);
}
}, (showErrorDialog));
isSubmitting = true;
Utils.showSubmitDialog(context);
}
_paymentRequestWithCard(BuildContext context) async {
isSubmitting = true;
Utils.showSubmitDialog(context);
await StripeApi.instance.createPaymentMethodFromCard(card)
.then((result) {
Utils.stripePaymentIntent(widget.order, null, result['id'], result['type'], (response) async {
if (response.data['status'] == Constants.STRIPE_STATUS_REQUIRES_CONFIRMATION) {
await StripeApi.instance.confirmPaymentIntent(
response.data[Constants.STRIPE_CLIENT_SECRET],
data: {
'payment_method': response.data['payment_method'],
}
).then((result2) {
if (result2['status'] == Constants.STRIPE_STATUS_SUCCEDED) {
Utils.stripeChargedSuccess(widget.order,
result['id'], // payment method id
result2['id'], (response) { // payment intent id
if (isSubmitting) {
Navigator.of(context).pop();
}
eventBus.fire(OnOrderUpdated());
Routes.router.navigateTo(context, '/orderdetail/${widget
.order.id}', replace: true);
},
(showErrorDialog)
);
} else {
showErrorDialog(Exception('Unknown error'));
}
}).catchError(showErrorDialog);
}
}, (showErrorDialog),
cardBrand: result['card']['brand'],
cardCountry: result['card']['country'],
cardExpMonth: result['card']['exp_month'],
cardExpYear: result['card']['exp_year'],
cardFunding: result['card']['funding'],
cardLast4: result['card']['last4'],
);
}).catchError(showErrorDialog);
}
void showErrorDialog(dynamic error) {
if (isSubmitting) {
Navigator.of(context).pop();
isSubmitting = false;
}
Utils.showMessageDialog(context, error, onOk: () {
Navigator.of(context).pop();
Navigator.of(context).pop();
});
}
}

View File

@@ -0,0 +1,206 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../../constants.dart';
import '../../generated/l10n.dart';
import '../../models/order.dart';
import '../../routes.dart';
import '../../store/actions.dart';
import '../../store/store.dart';
/// e-Transfer 支付页(替代原 Stripe 收款)。
/// 向顾客展示 e-Transfer 邮箱,并提醒在备注里写 R#<receiptId> 以便自动处理订单。
class ETransferPay extends StatelessWidget {
final Order order;
const ETransferPay({required this.order});
String get _remark =>
'${Constants.ETRANSFER_REMARK_PREFIX}${order.id}';
Future<void> _copy(BuildContext context, String text, String label) async {
await Clipboard.setData(ClipboardData(text: text));
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('$label copied')),
);
}
@override
Widget build(BuildContext context) {
store.dispatch(UpdateContext(context));
final isWide = MediaQuery.of(context).size.width > 700;
return Scaffold(
appBar: AppBar(
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios),
onPressed: () => Navigator.of(context).pop(),
),
title: const Text('e-Transfer Payment'),
),
body: Center(
child: ConstrainedBox(
constraints: BoxConstraints(
maxWidth: isWide ? 640 : double.infinity,
),
child: SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// 金额
Container(
padding: const EdgeInsets.symmetric(
vertical: 24, horizontal: 16),
decoration: BoxDecoration(
color: Colors.lightBlueAccent.withOpacity(0.12),
borderRadius: BorderRadius.circular(12),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
S.of(context).payment_amount,
style: const TextStyle(
fontSize: 15, color: Colors.black54),
),
Text(
'\$${order.totalPrice.toStringAsFixed(2)}',
style: const TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
),
),
],
),
),
const SizedBox(height: 24),
const Text(
'Please complete your payment via Interac e-Transfer:',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
),
const SizedBox(height: 16),
// 邮箱
_CopyRow(
icon: Icons.alternate_email,
title: 'Send e-Transfer to',
value: Constants.ETRANSFER_EMAIL,
onCopy: () => _copy(
context, Constants.ETRANSFER_EMAIL, 'Email'),
),
const SizedBox(height: 16),
// 备注
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFFFFF8E1),
border: Border.all(color: Colors.orangeAccent),
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Row(
children: [
Icon(Icons.priority_high, color: Colors.orange),
SizedBox(width: 8),
Text(
'Important — message / remark',
style: TextStyle(fontWeight: FontWeight.bold),
),
],
),
const SizedBox(height: 8),
const Text(
'In the e-Transfer message (remark) field, please include '
'the following code so we can match and process your order '
'automatically:',
),
const SizedBox(height: 12),
_CopyRow(
icon: Icons.receipt_long,
title: 'Order code',
value: _remark,
emphasize: true,
onCopy: () =>
_copy(context, _remark, 'Order code'),
),
],
),
),
const SizedBox(height: 24),
FilledButton.icon(
icon: const Icon(Icons.check_circle_outline),
label: const Text("I've sent the payment"),
style: FilledButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
),
onPressed: () {
Routes.router.navigateTo(context, '/orderdetail/${order.id}',
replace: true);
},
),
],
),
),
),
),
);
}
}
class _CopyRow extends StatelessWidget {
final IconData icon;
final String title;
final String value;
final VoidCallback onCopy;
final bool emphasize;
const _CopyRow({
required this.icon,
required this.title,
required this.value,
required this.onCopy,
this.emphasize = false,
});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
border: Border.all(color: Colors.black12),
borderRadius: BorderRadius.circular(12),
),
child: Row(
children: [
Icon(icon, color: Colors.lightBlueAccent),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title,
style: const TextStyle(
fontSize: 12, color: Colors.black54)),
Text(
value,
style: TextStyle(
fontSize: emphasize ? 20 : 16,
fontWeight:
emphasize ? FontWeight.bold : FontWeight.w600,
letterSpacing: emphasize ? 1.0 : 0,
),
),
],
),
),
IconButton(
icon: const Icon(Icons.copy, color: Colors.black54),
onPressed: onCopy,
),
],
),
);
}
}

View File

@@ -1,158 +0,0 @@
import 'package:flutter/material.dart';
import 'package:stripe_payment/stripe_payment.dart';
import '../../constants.dart';
import '../../events/eventbus.dart';
import '../../events/events.dart';
import '../../models/order.dart';
import '../../models/payment_platform.dart';
import '../../models/stripe_payment_method.dart';
import '../../routes.dart';
import '../../store/actions.dart';
import '../../store/store.dart';
import '../../utils/utils.dart';
class StripePay extends StatefulWidget {
final Key key;
final Order order;
final PaymentPlatform paymentPlatform;
final StripePaymentMethod stripePaymentMethod;
const StripePay(this.order, this.paymentPlatform, {this.key, this.stripePaymentMethod});
@override
State<StatefulWidget> createState() {
return StripePayState();
}
}
class StripePayState extends State<StripePay> {
GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey();
bool isSubmitting;
@override
Widget build(BuildContext context) {
store.dispatch(UpdateContext(context));
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
if (widget.stripePaymentMethod != null) {
_paymentWithPaymentMethod(context);
} else {
_paymentRequestWithCardForm(context);
}
});
return Scaffold(
key: _scaffoldKey,
body: Center(
child: Icon(
Icons.credit_card,
size: 40.0,
color: Colors.black26,
),
),
);
}
@override
void initState() {
super.initState();
isSubmitting = false;
StripePayment.setOptions(
StripeOptions(publishableKey: widget.paymentPlatform.publishableKey,
merchantId: widget.paymentPlatform.merchantId,
androidPayMode: 'test')
);
}
_paymentWithPaymentMethod(BuildContext context) async {
Utils.stripePaymentIntent(widget.order, widget.stripePaymentMethod.customerId,
widget.stripePaymentMethod.paymentMethodId,
widget.stripePaymentMethod.paymentMethodType, (response){
if (response.data['status'] == Constants.STRIPE_STATUS_REQUIRES_CONFIRMATION) {
StripePayment.confirmPaymentIntent(
PaymentIntent(
clientSecret: response.data[Constants.STRIPE_CLIENT_SECRET],
paymentMethodId: response.data['payment_method'],
),
).then((paymentIntentResult) {
if (paymentIntentResult.status == Constants.STRIPE_STATUS_SUCCEDED) {
Utils.stripeChargedSuccess(widget.order,
widget.stripePaymentMethod.paymentMethodId,
paymentIntentResult.paymentIntentId,
(response) {
eventBus.fire(OnOrderUpdated());
Routes.router.navigateTo(context, '/orderdetail/${widget
.order.id}', replace: true);
},
(showErrorDialog)
);
} else {
showErrorDialog(Exception('Unknown error'));
}
}).catchError(showErrorDialog);
}
}, (showErrorDialog));
isSubmitting = true;
Utils.showSubmitDialog(context);
}
_paymentRequestWithCardForm(BuildContext context) async {
StripePayment.paymentRequestWithCardForm(
CardFormPaymentRequest()
).then((paymentMethod) {
Utils.stripePaymentIntent(widget.order, null, paymentMethod.id, paymentMethod.type, (response){
if (response.data['status'] == Constants.STRIPE_STATUS_REQUIRES_CONFIRMATION) {
StripePayment.confirmPaymentIntent(
PaymentIntent(
clientSecret: response.data[Constants.STRIPE_CLIENT_SECRET],
paymentMethodId: response.data['payment_method'],
),
).then((paymentIntentResult) {
if (paymentIntentResult.status == Constants.STRIPE_STATUS_SUCCEDED) {
Utils.stripeChargedSuccess(widget.order,
paymentMethod.id,
paymentIntentResult.paymentIntentId,
(response) {
eventBus.fire(OnOrderUpdated());
Routes.router.navigateTo(context, '/orderdetail/${widget
.order.id}', replace: true);
},
(showErrorDialog)
);
} else {
showErrorDialog(Exception('Unknown error'));
}
}).catchError(showErrorDialog);
}
}, (showErrorDialog),
cardBrand: paymentMethod.card.brand,
cardCountry: paymentMethod.card.country,
cardExpMonth: paymentMethod.card.expMonth,
cardExpYear: paymentMethod.card.expYear,
cardFunding: paymentMethod.card.funding,
cardLast4: paymentMethod.card.last4,
);
}).catchError(showErrorDialog);
isSubmitting = true;
Utils.showSubmitDialog(context);
}
void showErrorDialog(dynamic error) {
if (isSubmitting) {
Navigator.of(context).pop();
isSubmitting = false;
}
Utils.showMessageDialog(context, error, onOk: () {
Navigator.of(context).pop();
Navigator.of(context).pop();
});
}
}

View File

@@ -1,32 +1,14 @@
import 'package:flutter/material.dart';
import '../../events/eventbus.dart';
import '../../events/events.dart';
import '../../constants.dart';
import '../../generated/l10n.dart';
import '../../models/stripe_payment_method.dart';
import '../../models/user.dart';
import '../../store/actions.dart';
import '../../store/store.dart';
import '../../utils/http_util.dart';
import '../../utils/utils.dart';
class MobileMyCards extends StatefulWidget {
final Key key;
const MobileMyCards({this.key});
@override
State<StatefulWidget> createState() {
return MyCardsState();
}
}
class MyCardsState extends State<MobileMyCards> {
User _user;
bool isSubmitting;
/// “My Cards” 页移动端已停止保存银行卡Stripe 移除)。改为展示 e-Transfer 说明。
class MobileMyCards extends StatelessWidget {
const MobileMyCards({Key? key});
@override
Widget build(BuildContext context) {
@@ -35,150 +17,62 @@ class MyCardsState extends State<MobileMyCards> {
return Scaffold(
appBar: AppBar(
leading: IconButton(
icon: Icon(Icons.arrow_back_ios),
onPressed: (){
Navigator.of(context).pop();
},
icon: const Icon(Icons.arrow_back_ios),
onPressed: () => Navigator.of(context).pop(),
),
title: Text(S.of(context).my_cards),
backgroundColor: Theme.of(context).primaryColor,
),
body: ListView.builder(
itemCount: _user.stripePaymentMethods.length,
itemBuilder: (BuildContext context, int position) {
StripePaymentMethod paymentMethod = _user.stripePaymentMethods[position];
return cardWidget(paymentMethod);
}
),
);
}
Widget cardWidget(StripePaymentMethod paymentMethod) {
return Container(
padding: EdgeInsets.only(
top: 16.0, left: 16.0, right: 16.0, bottom: 16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
paymentMethod.cardBrand == 'visa' ?
Image.asset(
'assets/images/visa.png',
width: 50.0,
height: 50.0,
fit: BoxFit.fill,
) : (paymentMethod.cardBrand == 'mastercard' ?
Image.asset(
'assets/images/master.png',
width: 50.0,
height: 50.0,
fit: BoxFit.fill,
) : Icon(
Icons.credit_card, size: 50.0, color: Colors.black38,)),
Expanded(
body: ListView(
children: [
Container(
margin: const EdgeInsets.all(16),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.lightBlueAccent.withOpacity(0.08),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.black12),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Container(
margin: EdgeInsets.only(left: 20.0, right: 10.0),
child: Text(
'**** ${paymentMethod.cardLast4}',
style: TextStyle(
fontSize: 20.0,
children: [
const Row(
children: [
Icon(Icons.info_outline, color: Colors.lightBlueAccent),
SizedBox(width: 8),
Expanded(
child: Text(
'Saved cards are no longer used',
style: TextStyle(
fontSize: 16, fontWeight: FontWeight.bold),
),
),
),
],
),
Container(
margin: EdgeInsets.only(left: 20.0, right: 10.0),
child: Text(
S.of(context).expire_token(paymentMethod.cardExpMonth, paymentMethod.cardExpYear),
style: TextStyle(
fontSize: 14.0,
color: Colors.black26,
),
),
const SizedBox(height: 12),
const Text(
'We have switched to Interac e-Transfer for payments. '
'You no longer need to save a credit card.',
),
const SizedBox(height: 16),
const Text('Send your e-Transfer to:'),
const SizedBox(height: 4),
SelectableText(
Constants.ETRANSFER_EMAIL,
style: const TextStyle(
fontSize: 16, fontWeight: FontWeight.bold),
),
const SizedBox(height: 12),
const Text(
'Remember to put your order code (e.g. R#<order id>) in the '
'e-Transfer message so we can process your order automatically.',
style: TextStyle(color: Colors.black54),
),
],
),
),
IconButton(
icon: Icon(Icons.clear, color: Colors.black26,),
onPressed: () {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text(S.of(context).warning),
content: Text(
S.of(context).are_you_sure_to_remove_the_card,
),
actions: <Widget>[
TextButton(
child: Text(S.of(context).cancel),
style: TextButton.styleFrom(
primary: Theme.of(context).primaryColor,
),
onPressed: () {
Navigator.of(context).pop();
},
),
TextButton(
child: Text(S.of(context).yes_i_am_sure),
onPressed: () {
Navigator.of(context).pop();
_removeCard(paymentMethod);
},
)
],
);
}
);
},
),
],
),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
width: 0.5,
color: Colors.black12,
),
),
),
);
}
_removeCard(StripePaymentMethod paymentMethod) {
HttpUtil.httpDelete('v1/stripe-card/${paymentMethod.id}', (response) {
_user = User.fromJson(response.data);
store.dispatch(UpdateCurrentUser(_user));
eventBus.fire(OnCurrentUserUpdated());
if (mounted) {
Navigator.of(context).pop();
setState(() {
isSubmitting = false;
});
}
}).catchError((error) {
if (isSubmitting) {
Navigator.of(context).pop();
isSubmitting = false;
}
Utils.showMessageDialog(context, error, onOk: () {
Navigator.of(context).pop();
Navigator.of(context).pop();
});
});
isSubmitting = true;
Utils.showSubmitDialog(context);
}
@override
void initState() {
super.initState();
setState(() {
isSubmitting = false;
_user = store.state.user;
});
}
}

View File

@@ -9,7 +9,6 @@ import '../../events/events.dart';
import '../../generated/l10n.dart';
import '../../models/order.dart';
import '../../models/payment_platform.dart';
import '../../models/stripe_payment_method.dart';
import '../../models/user.dart';
import '../../routes.dart';
import '../../store/actions.dart';
@@ -17,7 +16,6 @@ import '../../store/store.dart';
import '../../utils/http_util.dart';
import '../../utils/util_web.dart' if (dart.library.io) '../../utils/util_io.dart';
import '../../utils/utils.dart';
import '../../widgets/general/payment_verification_code_dialog.dart';
class MobilePayNow extends StatefulWidget {
final Key key;
@@ -36,7 +34,6 @@ class MobilePayNowState extends State<MobilePayNow> {
List<PaymentPlatform> paymentPlatforms;
User _user;
bool nativePay;
@override
Widget build(BuildContext context) {
@@ -56,7 +53,7 @@ class MobilePayNowState extends State<MobilePayNow> {
BuildContext mainContext = context;
ListView listView = ListView.builder(
itemCount: nativePay ? paymentPlatforms.length + 3 : paymentPlatforms.length + 2,
itemCount: paymentPlatforms.length + 1,
itemBuilder: (BuildContext context, int position) {
if (position == 0) {
return Column(
@@ -156,135 +153,7 @@ class MobilePayNowState extends State<MobilePayNow> {
);
}
PaymentPlatform paymentPlatform;
if (position == 1) {
if (_user.stripePaymentMethods.length > 0) {
paymentPlatform = paymentPlatforms[position - 1];
Column column = Column(
children: <Widget>[],
);
column.children.add(
Container(
padding: EdgeInsets.all(16.0),
width: double.infinity,
child: Text(
S.of(context).pay_with_existing_cards,
textAlign: TextAlign.left,
),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
width: 1,
color: Colors.black26,
),
),
),
),
);
for (StripePaymentMethod stripePaymentMethod in _user.stripePaymentMethods) {
column.children.add(
GestureDetector(
child: Container(
padding: EdgeInsets.only(
top: 16.0, left: 16.0, right: 16.0, bottom: 16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
stripePaymentMethod.cardBrand == 'visa' ?
Image.asset(
'assets/images/visa.png',
width: 50.0,
height: 50.0,
fit: BoxFit.fill,
) : (stripePaymentMethod.cardBrand == 'mastercard' ?
Image.asset(
'assets/images/master.png',
width: 50.0,
height: 50.0,
fit: BoxFit.fill,
) : Icon(
Icons.credit_card, size: 50.0, color: Colors.black38,)),
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Container(
margin: EdgeInsets.only(left: 20.0, right: 10.0),
child: Text(
'**** ${stripePaymentMethod.cardLast4}',
style: TextStyle(
fontSize: 20.0,
),
),
),
Container(
margin: EdgeInsets.only(left: 20.0, right: 10.0),
child: Text(
S.of(context).expire_token(stripePaymentMethod.cardExpMonth, stripePaymentMethod.cardExpYear),
style: TextStyle(
fontSize: 14.0,
color: Colors.black26,
),
),
),
],
),
),
Icon(
Icons.arrow_forward_ios,
color: Colors.black26,
),
],
),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
width: 0.5,
color: Colors.black12,
),
),
),
),
onTap: () {
showDialog(
context: mainContext,
builder: (BuildContext context) {
return PaymentVerificationCodeDialog(_user, () {
Util.goPayment(context, order, paymentPlatform,
stripePaymentMethod: stripePaymentMethod);
}, () {
});
},
);
},
),
);
}
column.children.add(
Container(
width: double.infinity,
child: SizedBox.shrink(),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
width: 10,
color: Colors.black26,
),
),
),
)
);
return column;
} else {
return SizedBox.shrink();
}
}
if (position == 2 && nativePay) {
paymentPlatform = paymentPlatforms[position - 2];
return Util().getNativePay(mainContext, order, paymentPlatform);
}
paymentPlatform = nativePay ? paymentPlatforms[position - 3] : paymentPlatforms[position - 2];
paymentPlatform = paymentPlatforms[position - 1];
return GestureDetector(
child: Container(
padding: EdgeInsets.only(top: 16.0, left: 16.0, right: 16.0, bottom: 16.0),
@@ -354,14 +223,6 @@ class MobilePayNowState extends State<MobilePayNow> {
).then((data) {
paymentPlatforms = (data['payment_platforms'] as List).map((e) =>
PaymentPlatform.fromJson(e)).toList();
PaymentPlatform pp = paymentPlatforms[0];
if (Constants.ENABLE_NATIVE_PAY && pp.publishableKey != null
&& pp.publishableKey.isNotEmpty && pp.merchantId != null
&& pp.merchantId.isNotEmpty) {
nativePay = true;
} else {
nativePay = false;
}
_user = User.fromJson(data['contact']);
store.dispatch(UpdateCurrentUser(_user));
eventBus.fire(OnCurrentUserUpdated());

View File

@@ -1,221 +0,0 @@
import 'package:flutter/material.dart';
import 'package:stripe_sdk/stripe_sdk.dart';
import 'package:stripe_sdk/stripe_sdk_ui.dart';
import '../../constants.dart';
import '../../events/eventbus.dart';
import '../../events/events.dart';
import '../../generated/l10n.dart';
import '../../models/order.dart';
import '../../models/payment_platform.dart';
import '../../models/stripe_payment_method.dart';
import '../../routes.dart';
import '../../store/actions.dart';
import '../../store/store.dart';
import '../../utils/utils.dart';
class MobileStripePayWeb extends StatefulWidget {
final Key key;
final Order order;
final PaymentPlatform paymentPlatform;
final StripePaymentMethod stripePaymentMethod;
const MobileStripePayWeb(this.order, this.paymentPlatform, {this.key, this.stripePaymentMethod});
@override
State<StatefulWidget> createState() {
return MobileStripePayWebState();
}
}
class MobileStripePayWebState extends State<MobileStripePayWeb> {
GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey();
final formKey = GlobalKey<FormState>();
final card = StripeCard();
CardForm form;
bool isSubmitting;
@override
Widget build(BuildContext context) {
store.dispatch(UpdateContext(context));
Widget body = Center(
child: Icon(
Icons.credit_card,
size: 40.0,
color: Colors.black26,
),
);
if (widget.stripePaymentMethod == null) {
form = CardForm(card: card, formKey: formKey, displayPostalCode: false,);
body = ListView(
children: <Widget>[form,],
);
}
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
if (widget.stripePaymentMethod != null) {
_paymentWithPaymentMethod(context);
}
});
return Scaffold(
key: _scaffoldKey,
appBar: AppBar(
leading: IconButton(
icon: Icon(Icons.arrow_back_ios),
onPressed: (){
Navigator.of(context).pop();
},
),
title: Text(S.of(context).add_credit_card),
backgroundColor: Theme.of(context).primaryColor,
actions: <Widget>[
IconButton(
icon: Icon(Icons.check),
onPressed: widget.stripePaymentMethod == null ? () {
if (formKey.currentState.validate()) {
formKey.currentState.save();
_paymentRequestWithCard(context);
} else {
ScaffoldMessenger.of(context).showSnackBar(
messageSnackBar(
context, S.of(context).this_credit_card_is_invalid
)
);
}
} : null,
),
],
),
body: body,
);
}
@override
void initState() {
super.initState();
isSubmitting = false;
StripeApi.init(widget.paymentPlatform.publishableKey);
}
SnackBar messageSnackBar(BuildContext context, String message) {
Column column = Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[Text(
message,
style: TextStyle(
color: Colors.white,
),
)],
);
return SnackBar(
content: Container(
height: 45.0,
child: column,
),
action: SnackBarAction(
label: S.of(context).ok,
onPressed: () {
ScaffoldMessenger.of(context).hideCurrentSnackBar();
},
),
);
}
_paymentWithPaymentMethod(BuildContext context) async {
Utils.stripePaymentIntent(widget.order, widget.stripePaymentMethod.customerId,
widget.stripePaymentMethod.paymentMethodId,
widget.stripePaymentMethod.paymentMethodType, (response) async {
if (response.data['status'] == Constants.STRIPE_STATUS_REQUIRES_CONFIRMATION) {
await StripeApi.instance.confirmPaymentIntent(
response.data[Constants.STRIPE_CLIENT_SECRET],
data: {
'payment_method': response.data['payment_method'],
},
).then((result2) {
if (result2['status'] == Constants.STRIPE_STATUS_SUCCEDED) {
Utils.stripeChargedSuccess(widget.order,
widget.stripePaymentMethod.paymentMethodId,
result2['id'], (response) {
if (isSubmitting) {
Navigator.of(context).pop();
}
eventBus.fire(OnOrderUpdated());
Routes.router.navigateTo(context, '/orderdetail/${widget
.order.id}', replace: true);
},
(showErrorDialog)
);
} else {
showErrorDialog(Exception('Unknown error'));
}
}).catchError(showErrorDialog);
}
}, (showErrorDialog));
isSubmitting = true;
Utils.showSubmitDialog(context);
}
_paymentRequestWithCard(BuildContext context) async {
isSubmitting = true;
Utils.showSubmitDialog(context);
await StripeApi.instance.createPaymentMethodFromCard(card)
.then((result) {
Utils.stripePaymentIntent(widget.order, null, result['id'], result['type'], (response) async {
if (response.data['status'] == Constants.STRIPE_STATUS_REQUIRES_CONFIRMATION) {
await StripeApi.instance.confirmPaymentIntent(
response.data[Constants.STRIPE_CLIENT_SECRET],
data: {
'payment_method': response.data['payment_method'],
}
).then((result2) {
if (result2['status'] == Constants.STRIPE_STATUS_SUCCEDED) {
Utils.stripeChargedSuccess(widget.order,
result['id'], // payment method id
result2['id'], (response) { // payment intent id
if (isSubmitting) {
Navigator.of(context).pop();
}
eventBus.fire(OnOrderUpdated());
Routes.router.navigateTo(context, '/orderdetail/${widget
.order.id}', replace: true);
},
(showErrorDialog)
);
} else {
showErrorDialog(Exception('Unknown error'));
}
}).catchError(showErrorDialog);
}
}, (showErrorDialog),
cardBrand: result['card']['brand'],
cardCountry: result['card']['country'],
cardExpMonth: result['card']['exp_month'],
cardExpYear: result['card']['exp_year'],
cardFunding: result['card']['funding'],
cardLast4: result['card']['last4'],
);
}).catchError(showErrorDialog);
}
void showErrorDialog(dynamic error) {
if (isSubmitting) {
Navigator.of(context).pop();
isSubmitting = false;
}
Utils.showMessageDialog(context, error, onOk: () {
Navigator.of(context).pop();
Navigator.of(context).pop();
});
}
}

View File

@@ -18,7 +18,8 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
version: 1.0.1+2
environment:
sdk: ">=2.7.0 <3.0.0"
sdk: ">=3.0.0 <4.0.0"
flutter: ">=3.29.0"
dependencies:
flutter:
@@ -77,13 +78,7 @@ dependencies:
toggle_switch: ^1.2.0
pinput: ^1.2.0
google_maps_flutter: ^2.0.6 #^1.2.0
stripe_payment: ^1.0.10
# stripe_sdk:
# git:
# url: git://github.com/romme86/stripe-sdk.git
stripe_sdk:
git:
url: https://github.com/ezet/stripe-sdk.git
# Stripe removed — switched to e-Transfer. See UPGRADE_NOTES.md.
uuid: ^3.0.4 #^2.2.2
ffi: ^1.0.0
# firebase_messaging: ^10.0.4 #^7.0.3