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

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();
});
}
}