Skip to content

Commit

Permalink
Allow supplying custom card information, fixes #213, #40 (#221)
Browse files Browse the repository at this point in the history
* Allow supplying custom card information, fixes #213, #40

* reset Podfile.lock

* highlight PCI compliance issues

* add extra warning
  • Loading branch information
jonasbark authored Jul 23, 2021
1 parent ae51739 commit e50d4a5
Show file tree
Hide file tree
Showing 13 changed files with 559 additions and 11 deletions.
183 changes: 183 additions & 0 deletions example/lib/screens/custom_card_payment_screen.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:flutter_stripe/flutter_stripe.dart';
import 'package:http/http.dart' as http;
import 'package:stripe_example/widgets/loading_button.dart';
import 'package:stripe_platform_interface/stripe_platform_interface.dart';

import '../config.dart';

class CustomCardPaymentScreen extends StatefulWidget {
@override
_CustomCardPaymentScreenState createState() =>
_CustomCardPaymentScreenState();
}

class _CustomCardPaymentScreenState extends State<CustomCardPaymentScreen> {
CardDetails _card = CardDetails();
String _email = '';
bool? _saveCard = false;

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Padding(
padding: EdgeInsets.all(16),
child: Text(
'If you don\'t want to or can\'t rely on the CardField you'
' can use the dangerouslyUpdateCardDetails in combination with '
'your own card field implementation. '
'Please beware that this will potentially break PCI compliance: '
'https://stripe.com/docs/security/guide#validating-pci-compliance')),
Padding(
padding: EdgeInsets.all(16),
child: TextField(
decoration: InputDecoration(hintText: 'Email'),
onChanged: (value) {
setState(() {
_email = value;
});
},
),
),
Padding(
padding: EdgeInsets.all(16),
child: Row(
children: [
Expanded(
flex: 2,
child: TextField(
decoration: InputDecoration(hintText: 'Number'),
onChanged: (number) {
setState(() {
_card = _card.copyWith(number: number);
});
},
keyboardType: TextInputType.number,
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 4),
width: 80,
child: TextField(
decoration: InputDecoration(hintText: 'Exp. Year'),
onChanged: (number) {
setState(() {
_card = _card.copyWith(
expirationYear: int.tryParse(number));
});
},
keyboardType: TextInputType.number,
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 4),
width: 80,
child: TextField(
decoration: InputDecoration(hintText: 'Exp. Month'),
onChanged: (number) {
setState(() {
_card = _card.copyWith(
expirationMonth: int.tryParse(number));
});
},
keyboardType: TextInputType.number,
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 4),
width: 80,
child: TextField(
decoration: InputDecoration(hintText: 'CVC'),
onChanged: (number) {
setState(() {
_card = _card.copyWith(cvc: number);
});
},
keyboardType: TextInputType.number,
),
),
],
),
),
CheckboxListTile(
value: _saveCard,
onChanged: (value) {
setState(() {
_saveCard = value;
});
},
title: Text('Save card during payment'),
),
Padding(
padding: EdgeInsets.all(16),
child: LoadingButton(
onPressed: _handlePayPress,
text: 'Pay',
),
),
],
),
);
}

Future<void> _handlePayPress() async {
await Stripe.instance.dangerouslyUpdateCardDetails(_card);

// 1. fetch Intent Client Secret from backend
final clientSecret = await fetchPaymentIntentClientSecret();

// 2. Gather customer billing information (ex. email)
final billingDetails = BillingDetails(
email: '[email protected]',
phone: '+48888000888',
address: Address(
city: 'Houston',
country: 'US',
line1: '1459 Circle Drive',
line2: '',
state: 'Texas',
postalCode: '77063',
),
); // mo mocked data for tests

// 3. Confirm payment with card details
// The rest will be done automatically using CustomCards
// ignore: unused_local_variable
final paymentIntent = await Stripe.instance.confirmPayment(
clientSecret['clientSecret'],
PaymentMethodParams.card(
billingDetails: billingDetails,
setupFutureUsage:
_saveCard == true ? PaymentIntentsFutureUsage.OffSession : null,
),
);

ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text('Success!: The payment was confirmed successfully!')));
}

Future<Map<String, dynamic>> fetchPaymentIntentClientSecret() async {
final url = Uri.parse('$kApiUrl/create-payment-intent');
final response = await http.post(
url,
headers: {
'Content-Type': 'application/json',
},
body: json.encode({
'email': _email,
'currency': 'usd',
'items': [
{'id': 'id'}
],
'request_three_d_secure': 'any',
}),
);
return json.decode(response.body);
}
}
7 changes: 6 additions & 1 deletion example/lib/screens/screens.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:stripe_example/screens/apple_pay_screen.dart';
import 'package:stripe_example/screens/custom_card_payment_screen.dart';
import 'package:stripe_example/screens/google_pay_screen.dart';

import '../screens/no_webhook_payment_screen.dart';
Expand Down Expand Up @@ -28,6 +29,10 @@ class Example {
title: 'Card payment without webhooks',
builder: (c) => NoWebhookPaymentScreen(),
),
Example(
title: 'Card payment with Flutter native card input (not PCI compliant)',
builder: (c) => CustomCardPaymentScreen(),
),
Example(
title: 'Apple Pay payment (iOS)',
builder: (c) => ApplePayScreen(),
Expand All @@ -54,7 +59,7 @@ class Example {
),
Example(
title: 'Create token (legacy)',
builder: (context)=> LegacyTokenScreen(),
builder: (context) => LegacyTokenScreen(),
)
];
}
10 changes: 10 additions & 0 deletions packages/stripe/lib/src/stripe.dart
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,16 @@ class Stripe {
return await _platform.confirmPaymentSheetPayment();
}

/// Updates the internal card details. This method will not validate the card
/// information so you should validate the information yourself.
/// WARNING!!! Only do this if you're certain that you fulfill the necessary
/// PCI compliance requirements. Make sure that you're not mistakenly logging
/// or storing full card details! See the docs for
/// details: https://stripe.com/docs/security/guide#validating-pci-compliance
Future<void> dangerouslyUpdateCardDetails(CardDetails card) async {
return await _platform.dangerouslyUpdateCardDetails(card);
}

FutureOr<void> _awaitForSettings() {
if (_needsSettings) {
_settingsFuture = applySettings();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import androidx.annotation.NonNull
import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReadableMap
import com.facebook.react.uimanager.ThemedReactContext
import com.reactnativestripesdk.StripeSdkCardViewManager
import com.reactnativestripesdk.StripeSdkModule
import io.flutter.embedding.android.FlutterFragmentActivity
Expand Down Expand Up @@ -42,9 +43,9 @@ class StripeAndroidPlugin : FlutterPlugin, MethodCallHandler, ActivityAware {
override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
if (!this::stripeSdk.isInitialized) {
result.error(
"flutter_stripe initialization failed",
"The plugin failed to initialize. Are you using FlutterFragmentActivity? Please check the README: https://github.com/flutter-stripe/flutter_stripe#android",
null
"flutter_stripe initialization failed",
"The plugin failed to initialize. Are you using FlutterFragmentActivity? Please check the README: https://github.com/flutter-stripe/flutter_stripe#android",
null
)
return
}
Expand Down Expand Up @@ -99,6 +100,13 @@ class StripeAndroidPlugin : FlutterPlugin, MethodCallHandler, ActivityAware {
promise = Promise(result),
params = call.requiredArgument("params")
)
"dangerouslyUpdateCardDetails" -> {
stripeSdkCardViewManager.setCardDetails(
value = call.requiredArgument("params"),
reactContext = ThemedReactContext(stripeSdk.currentActivity.activity, channel)
)
result.success(null)
}
/*"registerConfirmSetupIntentCallbacks" -> stripeSdk.registerConfirmSetupIntentCallbacks(
successCallback = Promise(result),
errorCallback = Promise(result),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import com.facebook.react.common.MapBuilder
import com.facebook.react.uimanager.SimpleViewManager
import com.facebook.react.uimanager.ThemedReactContext
import com.facebook.react.uimanager.annotations.ReactProp
import com.stripe.android.model.PaymentMethodCreateParams

const val CARD_FIELD_INSTANCE_NAME = "CardFieldInstance"

Expand Down Expand Up @@ -79,4 +80,19 @@ class StripeSdkCardViewManager : SimpleViewManager<StripeSdkCardView>() {
}
return null
}

fun setCardDetails(value: ReadableMap, reactContext: ThemedReactContext) {
val number = getValOr(value, "number", null)
val expirationYear = getIntOrNull(value, "expirationYear")
val expirationMonth = getIntOrNull(value, "expirationMonth")
val cvc = getValOr(value, "cvc", null)

val cardViewInstance = getCardViewInstance() ?: createViewInstance(reactContext)
cardViewInstance.cardParams = PaymentMethodCreateParams.Card.Builder()
.setNumber(number)
.setCvc(cvc)
.setExpiryMonth(expirationMonth)
.setExpiryYear(expirationYear)
.build()
}
}
26 changes: 19 additions & 7 deletions packages/stripe_ios/ios/Classes/CardFieldView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ protocol CardFieldDelegate {

protocol CardFieldManager {
func getCardFieldReference(id: String) -> Any?
func setCardDetails(value: NSDictionary) -> Void
}

public class CardFieldViewFactory: NSObject, FlutterPlatformViewFactory, CardFieldDelegate, CardFieldManager {
Expand Down Expand Up @@ -52,17 +53,28 @@ public class CardFieldViewFactory: NSObject, FlutterPlatformViewFactory, CardFie

public let cardFieldMap: NSMutableDictionary = [:]

func onDidCreateViewInstance(id: String, reference: Any?) -> Void {
cardFieldMap[id] = reference
}
func onDidCreateViewInstance(id: String, reference: Any?) -> Void {
cardFieldMap[id] = reference
}

func onDidDestroyViewInstance(id: String) {
func onDidDestroyViewInstance(id: String) {
cardFieldMap[id] = nil
}
}

public func getCardFieldReference(id: String) -> Any? {
public func getCardFieldReference(id: String) -> Any? {
return self.cardFieldMap[id]
}
}

public func setCardDetails(value: NSDictionary) {
let cardField: CardFieldView? = self.getCardFieldReference(id: CARD_FIELD_INSTANCE_ID) as? CardFieldView ?? self.create(withFrame: CGRect.zero, viewIdentifier: -1, arguments: nil) as? CardFieldView

let cardParams = STPPaymentMethodCardParams()
cardParams.cvc = value["cvc"] as? String
cardParams.number = value["number"] as? String
cardParams.expYear = value["expirationYear"] as? NSNumber
cardParams.expMonth = value["expirationMonth"] as? NSNumber
cardField?.cardParams = cardParams
}
}


Expand Down
13 changes: 13 additions & 0 deletions packages/stripe_ios/ios/Classes/StripePlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ public class StripePlugin: StripeSdk, FlutterPlugin {
return createPaymentMethod(call, result: result)
case "createToken":
return createToken(call, result: result)
case "dangerouslyUpdateCardDetails":
return dangerouslyUpdateCardDetails(call, result: result)
default:
result(FlutterMethodNotImplemented)
}
Expand Down Expand Up @@ -297,4 +299,15 @@ extension StripePlugin {
rejecter: rejecter(for: result))
}

public func dangerouslyUpdateCardDetails(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
guard let arguments = call.arguments as? FlutterMap,
let params = arguments["params"] as? NSDictionary else {
result(FlutterError.invalidParams)
return
}
let cardFieldUIManager = bridge.module(forName: "CardFieldManager")
cardFieldUIManager?.setCardDetails(value: params)

result(nil)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import 'package:stripe_platform_interface/src/result_parser.dart';

import 'models/app_info.dart';
import 'models/apple_pay.dart';
import 'models/card_details.dart';
import 'models/errors.dart';
import 'models/payment_intents.dart';
import 'models/payment_methods.dart';
Expand Down Expand Up @@ -206,6 +207,13 @@ class MethodChannelStripe extends StripePlatform {
.parse(result: result!, successResultKey: 'token');
}

@override
Future<void> dangerouslyUpdateCardDetails(CardDetails card) async {
await _methodChannel.invokeMethod('dangerouslyUpdateCardDetails', {
'params': card.toJson(),
});
}

void _parsePaymentSheetResult(Map<String, dynamic>? result) {
if (result != null) {
if (result.isEmpty) {
Expand Down
Loading

0 comments on commit e50d4a5

Please sign in to comment.