Skip to content

Commit e50d4a5

Browse files
authored
Allow supplying custom card information, fixes #213, #40 (#221)
* Allow supplying custom card information, fixes #213, #40 * reset Podfile.lock * highlight PCI compliance issues * add extra warning
1 parent ae51739 commit e50d4a5

File tree

13 files changed

+559
-11
lines changed

13 files changed

+559
-11
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
import 'dart:convert';
2+
3+
import 'package:flutter/material.dart';
4+
import 'package:flutter_stripe/flutter_stripe.dart';
5+
import 'package:http/http.dart' as http;
6+
import 'package:stripe_example/widgets/loading_button.dart';
7+
import 'package:stripe_platform_interface/stripe_platform_interface.dart';
8+
9+
import '../config.dart';
10+
11+
class CustomCardPaymentScreen extends StatefulWidget {
12+
@override
13+
_CustomCardPaymentScreenState createState() =>
14+
_CustomCardPaymentScreenState();
15+
}
16+
17+
class _CustomCardPaymentScreenState extends State<CustomCardPaymentScreen> {
18+
CardDetails _card = CardDetails();
19+
String _email = '';
20+
bool? _saveCard = false;
21+
22+
@override
23+
Widget build(BuildContext context) {
24+
return Scaffold(
25+
appBar: AppBar(),
26+
body: Column(
27+
crossAxisAlignment: CrossAxisAlignment.stretch,
28+
children: [
29+
Padding(
30+
padding: EdgeInsets.all(16),
31+
child: Text(
32+
'If you don\'t want to or can\'t rely on the CardField you'
33+
' can use the dangerouslyUpdateCardDetails in combination with '
34+
'your own card field implementation. '
35+
'Please beware that this will potentially break PCI compliance: '
36+
'https://stripe.com/docs/security/guide#validating-pci-compliance')),
37+
Padding(
38+
padding: EdgeInsets.all(16),
39+
child: TextField(
40+
decoration: InputDecoration(hintText: 'Email'),
41+
onChanged: (value) {
42+
setState(() {
43+
_email = value;
44+
});
45+
},
46+
),
47+
),
48+
Padding(
49+
padding: EdgeInsets.all(16),
50+
child: Row(
51+
children: [
52+
Expanded(
53+
flex: 2,
54+
child: TextField(
55+
decoration: InputDecoration(hintText: 'Number'),
56+
onChanged: (number) {
57+
setState(() {
58+
_card = _card.copyWith(number: number);
59+
});
60+
},
61+
keyboardType: TextInputType.number,
62+
),
63+
),
64+
Container(
65+
padding: const EdgeInsets.symmetric(horizontal: 4),
66+
width: 80,
67+
child: TextField(
68+
decoration: InputDecoration(hintText: 'Exp. Year'),
69+
onChanged: (number) {
70+
setState(() {
71+
_card = _card.copyWith(
72+
expirationYear: int.tryParse(number));
73+
});
74+
},
75+
keyboardType: TextInputType.number,
76+
),
77+
),
78+
Container(
79+
padding: const EdgeInsets.symmetric(horizontal: 4),
80+
width: 80,
81+
child: TextField(
82+
decoration: InputDecoration(hintText: 'Exp. Month'),
83+
onChanged: (number) {
84+
setState(() {
85+
_card = _card.copyWith(
86+
expirationMonth: int.tryParse(number));
87+
});
88+
},
89+
keyboardType: TextInputType.number,
90+
),
91+
),
92+
Container(
93+
padding: const EdgeInsets.symmetric(horizontal: 4),
94+
width: 80,
95+
child: TextField(
96+
decoration: InputDecoration(hintText: 'CVC'),
97+
onChanged: (number) {
98+
setState(() {
99+
_card = _card.copyWith(cvc: number);
100+
});
101+
},
102+
keyboardType: TextInputType.number,
103+
),
104+
),
105+
],
106+
),
107+
),
108+
CheckboxListTile(
109+
value: _saveCard,
110+
onChanged: (value) {
111+
setState(() {
112+
_saveCard = value;
113+
});
114+
},
115+
title: Text('Save card during payment'),
116+
),
117+
Padding(
118+
padding: EdgeInsets.all(16),
119+
child: LoadingButton(
120+
onPressed: _handlePayPress,
121+
text: 'Pay',
122+
),
123+
),
124+
],
125+
),
126+
);
127+
}
128+
129+
Future<void> _handlePayPress() async {
130+
await Stripe.instance.dangerouslyUpdateCardDetails(_card);
131+
132+
// 1. fetch Intent Client Secret from backend
133+
final clientSecret = await fetchPaymentIntentClientSecret();
134+
135+
// 2. Gather customer billing information (ex. email)
136+
final billingDetails = BillingDetails(
137+
138+
phone: '+48888000888',
139+
address: Address(
140+
city: 'Houston',
141+
country: 'US',
142+
line1: '1459 Circle Drive',
143+
line2: '',
144+
state: 'Texas',
145+
postalCode: '77063',
146+
),
147+
); // mo mocked data for tests
148+
149+
// 3. Confirm payment with card details
150+
// The rest will be done automatically using CustomCards
151+
// ignore: unused_local_variable
152+
final paymentIntent = await Stripe.instance.confirmPayment(
153+
clientSecret['clientSecret'],
154+
PaymentMethodParams.card(
155+
billingDetails: billingDetails,
156+
setupFutureUsage:
157+
_saveCard == true ? PaymentIntentsFutureUsage.OffSession : null,
158+
),
159+
);
160+
161+
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
162+
content: Text('Success!: The payment was confirmed successfully!')));
163+
}
164+
165+
Future<Map<String, dynamic>> fetchPaymentIntentClientSecret() async {
166+
final url = Uri.parse('$kApiUrl/create-payment-intent');
167+
final response = await http.post(
168+
url,
169+
headers: {
170+
'Content-Type': 'application/json',
171+
},
172+
body: json.encode({
173+
'email': _email,
174+
'currency': 'usd',
175+
'items': [
176+
{'id': 'id'}
177+
],
178+
'request_three_d_secure': 'any',
179+
}),
180+
);
181+
return json.decode(response.body);
182+
}
183+
}

example/lib/screens/screens.dart

+6-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import 'package:flutter/material.dart';
22
import 'package:stripe_example/screens/apple_pay_screen.dart';
3+
import 'package:stripe_example/screens/custom_card_payment_screen.dart';
34
import 'package:stripe_example/screens/google_pay_screen.dart';
45

56
import '../screens/no_webhook_payment_screen.dart';
@@ -28,6 +29,10 @@ class Example {
2829
title: 'Card payment without webhooks',
2930
builder: (c) => NoWebhookPaymentScreen(),
3031
),
32+
Example(
33+
title: 'Card payment with Flutter native card input (not PCI compliant)',
34+
builder: (c) => CustomCardPaymentScreen(),
35+
),
3136
Example(
3237
title: 'Apple Pay payment (iOS)',
3338
builder: (c) => ApplePayScreen(),
@@ -54,7 +59,7 @@ class Example {
5459
),
5560
Example(
5661
title: 'Create token (legacy)',
57-
builder: (context)=> LegacyTokenScreen(),
62+
builder: (context) => LegacyTokenScreen(),
5863
)
5964
];
6065
}

packages/stripe/lib/src/stripe.dart

+10
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,16 @@ class Stripe {
316316
return await _platform.confirmPaymentSheetPayment();
317317
}
318318

319+
/// Updates the internal card details. This method will not validate the card
320+
/// information so you should validate the information yourself.
321+
/// WARNING!!! Only do this if you're certain that you fulfill the necessary
322+
/// PCI compliance requirements. Make sure that you're not mistakenly logging
323+
/// or storing full card details! See the docs for
324+
/// details: https://stripe.com/docs/security/guide#validating-pci-compliance
325+
Future<void> dangerouslyUpdateCardDetails(CardDetails card) async {
326+
return await _platform.dangerouslyUpdateCardDetails(card);
327+
}
328+
319329
FutureOr<void> _awaitForSettings() {
320330
if (_needsSettings) {
321331
_settingsFuture = applySettings();

packages/stripe_android/android/src/main/kotlin/com/flutter/stripe/StripeAndroidPlugin.kt

+11-3
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import androidx.annotation.NonNull
44
import com.facebook.react.bridge.Promise
55
import com.facebook.react.bridge.ReactApplicationContext
66
import com.facebook.react.bridge.ReadableMap
7+
import com.facebook.react.uimanager.ThemedReactContext
78
import com.reactnativestripesdk.StripeSdkCardViewManager
89
import com.reactnativestripesdk.StripeSdkModule
910
import io.flutter.embedding.android.FlutterFragmentActivity
@@ -42,9 +43,9 @@ class StripeAndroidPlugin : FlutterPlugin, MethodCallHandler, ActivityAware {
4243
override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
4344
if (!this::stripeSdk.isInitialized) {
4445
result.error(
45-
"flutter_stripe initialization failed",
46-
"The plugin failed to initialize. Are you using FlutterFragmentActivity? Please check the README: https://github.com/flutter-stripe/flutter_stripe#android",
47-
null
46+
"flutter_stripe initialization failed",
47+
"The plugin failed to initialize. Are you using FlutterFragmentActivity? Please check the README: https://github.com/flutter-stripe/flutter_stripe#android",
48+
null
4849
)
4950
return
5051
}
@@ -99,6 +100,13 @@ class StripeAndroidPlugin : FlutterPlugin, MethodCallHandler, ActivityAware {
99100
promise = Promise(result),
100101
params = call.requiredArgument("params")
101102
)
103+
"dangerouslyUpdateCardDetails" -> {
104+
stripeSdkCardViewManager.setCardDetails(
105+
value = call.requiredArgument("params"),
106+
reactContext = ThemedReactContext(stripeSdk.currentActivity.activity, channel)
107+
)
108+
result.success(null)
109+
}
102110
/*"registerConfirmSetupIntentCallbacks" -> stripeSdk.registerConfirmSetupIntentCallbacks(
103111
successCallback = Promise(result),
104112
errorCallback = Promise(result),

packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/StripeSdkCardViewManager.kt

+16
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import com.facebook.react.common.MapBuilder
66
import com.facebook.react.uimanager.SimpleViewManager
77
import com.facebook.react.uimanager.ThemedReactContext
88
import com.facebook.react.uimanager.annotations.ReactProp
9+
import com.stripe.android.model.PaymentMethodCreateParams
910

1011
const val CARD_FIELD_INSTANCE_NAME = "CardFieldInstance"
1112

@@ -79,4 +80,19 @@ class StripeSdkCardViewManager : SimpleViewManager<StripeSdkCardView>() {
7980
}
8081
return null
8182
}
83+
84+
fun setCardDetails(value: ReadableMap, reactContext: ThemedReactContext) {
85+
val number = getValOr(value, "number", null)
86+
val expirationYear = getIntOrNull(value, "expirationYear")
87+
val expirationMonth = getIntOrNull(value, "expirationMonth")
88+
val cvc = getValOr(value, "cvc", null)
89+
90+
val cardViewInstance = getCardViewInstance() ?: createViewInstance(reactContext)
91+
cardViewInstance.cardParams = PaymentMethodCreateParams.Card.Builder()
92+
.setNumber(number)
93+
.setCvc(cvc)
94+
.setExpiryMonth(expirationMonth)
95+
.setExpiryYear(expirationYear)
96+
.build()
97+
}
8298
}

packages/stripe_ios/ios/Classes/CardFieldView.swift

+19-7
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ protocol CardFieldDelegate {
1818

1919
protocol CardFieldManager {
2020
func getCardFieldReference(id: String) -> Any?
21+
func setCardDetails(value: NSDictionary) -> Void
2122
}
2223

2324
public class CardFieldViewFactory: NSObject, FlutterPlatformViewFactory, CardFieldDelegate, CardFieldManager {
@@ -52,17 +53,28 @@ public class CardFieldViewFactory: NSObject, FlutterPlatformViewFactory, CardFie
5253

5354
public let cardFieldMap: NSMutableDictionary = [:]
5455

55-
func onDidCreateViewInstance(id: String, reference: Any?) -> Void {
56-
cardFieldMap[id] = reference
57-
}
56+
func onDidCreateViewInstance(id: String, reference: Any?) -> Void {
57+
cardFieldMap[id] = reference
58+
}
5859

59-
func onDidDestroyViewInstance(id: String) {
60+
func onDidDestroyViewInstance(id: String) {
6061
cardFieldMap[id] = nil
61-
}
62+
}
6263

63-
public func getCardFieldReference(id: String) -> Any? {
64+
public func getCardFieldReference(id: String) -> Any? {
6465
return self.cardFieldMap[id]
65-
}
66+
}
67+
68+
public func setCardDetails(value: NSDictionary) {
69+
let cardField: CardFieldView? = self.getCardFieldReference(id: CARD_FIELD_INSTANCE_ID) as? CardFieldView ?? self.create(withFrame: CGRect.zero, viewIdentifier: -1, arguments: nil) as? CardFieldView
70+
71+
let cardParams = STPPaymentMethodCardParams()
72+
cardParams.cvc = value["cvc"] as? String
73+
cardParams.number = value["number"] as? String
74+
cardParams.expYear = value["expirationYear"] as? NSNumber
75+
cardParams.expMonth = value["expirationMonth"] as? NSNumber
76+
cardField?.cardParams = cardParams
77+
}
6678
}
6779

6880

packages/stripe_ios/ios/Classes/StripePlugin.swift

+13
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,8 @@ public class StripePlugin: StripeSdk, FlutterPlugin {
7171
return createPaymentMethod(call, result: result)
7272
case "createToken":
7373
return createToken(call, result: result)
74+
case "dangerouslyUpdateCardDetails":
75+
return dangerouslyUpdateCardDetails(call, result: result)
7476
default:
7577
result(FlutterMethodNotImplemented)
7678
}
@@ -297,4 +299,15 @@ extension StripePlugin {
297299
rejecter: rejecter(for: result))
298300
}
299301

302+
public func dangerouslyUpdateCardDetails(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
303+
guard let arguments = call.arguments as? FlutterMap,
304+
let params = arguments["params"] as? NSDictionary else {
305+
result(FlutterError.invalidParams)
306+
return
307+
}
308+
let cardFieldUIManager = bridge.module(forName: "CardFieldManager")
309+
cardFieldUIManager?.setCardDetails(value: params)
310+
311+
result(nil)
312+
}
300313
}

packages/stripe_platform_interface/lib/src/method_channel_stripe.dart

+8
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import 'package:stripe_platform_interface/src/result_parser.dart';
66

77
import 'models/app_info.dart';
88
import 'models/apple_pay.dart';
9+
import 'models/card_details.dart';
910
import 'models/errors.dart';
1011
import 'models/payment_intents.dart';
1112
import 'models/payment_methods.dart';
@@ -206,6 +207,13 @@ class MethodChannelStripe extends StripePlatform {
206207
.parse(result: result!, successResultKey: 'token');
207208
}
208209

210+
@override
211+
Future<void> dangerouslyUpdateCardDetails(CardDetails card) async {
212+
await _methodChannel.invokeMethod('dangerouslyUpdateCardDetails', {
213+
'params': card.toJson(),
214+
});
215+
}
216+
209217
void _parsePaymentSheetResult(Map<String, dynamic>? result) {
210218
if (result != null) {
211219
if (result.isEmpty) {

0 commit comments

Comments
 (0)