0.0.6 release

This commit is contained in:
Nathan Anderson 2023-12-01 16:12:42 -07:00
parent d7d27a1cf5
commit 462e40308f
9 changed files with 581 additions and 421 deletions

View File

@ -1,3 +1,13 @@
## 0.0.6
- Improved assertion and error messaging when missing stripe implements
- Added better doc comments
- Fixed `CardTextField.delayToShowLoading`, now it uses it
- Fixed bad assertion logic when providing stripe keys
- Added ability to make Stripe call with `GlobalKey`
- Refactored method `onTokenReceived` to `onStripeResponse` to be clearer
- Refactored method `onCardDetailsComplete` to `onValidCardDetails` to be clearer
## 0.0.5 ## 0.0.5
- Fix Web, invalid call to `Platform.isAndroid` - Fix Web, invalid call to `Platform.isAndroid`

View File

@ -23,7 +23,7 @@ Got to use emojis and taglines for attention grabbing and algorithm hacking:
- Native Implementation: compiles and loads like the rest of your app, unlike embeded html - Native Implementation: compiles and loads like the rest of your app, unlike embeded html
- Automatic validation: no `inputFormatters` or `RegExp` needed on your side - Automatic validation: no `inputFormatters` or `RegExp` needed on your side
The card data can either be retrieved with the `onCardDetailsComplete` callback, or The card data can either be retrieved with the `onValidCardDetails` callback, or
you can have the element automatically create a Stripe card token when the fields you can have the element automatically create a Stripe card token when the fields
are filled out, and return the token with the `onTokenReceived` callback. are filled out, and return the token with the `onTokenReceived` callback.
@ -62,12 +62,13 @@ Include the package in a file:
import 'package:stripe_native_card_field/stripe_native_card_field.dart'; import 'package:stripe_native_card_field/stripe_native_card_field.dart';
``` ```
### For just Card Data ### For Raw Card Data
Provide a callback for the `CardTextField` to return you the data when its complete.
```dart ```dart
CardTextField( CardTextField(
width: 500, width: 500,
onCardDetailsComplete: (details) { onValidCardDetails: (details) {
// Save the card details to use with your call to Stripe, or whoever // Save the card details to use with your call to Stripe, or whoever
setState(() => _cardDetails = details); setState(() => _cardDetails = details);
}, },
@ -76,17 +77,27 @@ CardTextField(
### For Stripe Token ### For Stripe Token
Simply provide a function for the `onStripeResponse` callback!
```dart ```dart
CardTextField( CardTextField(
width: 500, width: 500,
stripePublishableKey: 'pk_test_abc123', // Your stripe key here stripePublishableKey: 'pk_test_abc123', // Your stripe key here
onTokenReceived: (token) { onStripeResponse: (Map<String, dynamic> data) {
// Save the stripe token to send to your backend // Save the stripe token to send to your backend
setState(() => _token = token); setState(() => _tokenData = data);
}, },
); );
``` ```
If you want more fine-grained control of when the stripe call is made, you
can create a `GlobalKey` and access the `CardTextFieldState`, calling the
`getStripeResponse()` function yourself. See the provided [example](https://pub.dev/packages/stripe_native_card_field/example)
for details. If you choose this route, do not provide an `onStripeResponse` callback, or you will end up
making two calls to stripe!
# Additional information # Additional information
Repository located [here](https://git.fosscat.com/n8r/stripe_native_card_field) Repository located [here](https://git.fosscat.com/n8r/stripe_native_card_field)
Please email me at n8r@fosscat.com for any issues or PRs.

View File

@ -61,14 +61,15 @@ class _MyHomePageState extends State<MyHomePage> {
), ),
CardTextField( CardTextField(
width: 300, width: 300,
onCardDetailsComplete: (details) { onValidCardDetails: (details) {
if (kDebugMode) { if (kDebugMode) {
print(details); print(details);
} }
}, },
textStyle: textStyle:
const TextStyle(fontFamily: 'Lato', color: Colors.tealAccent), const TextStyle(fontFamily: 'Lato', color: Colors.tealAccent),
hintTextStyle: const TextStyle(fontFamily: 'Lato', color: Colors.teal), hintTextStyle:
const TextStyle(fontFamily: 'Lato', color: Colors.teal),
errorTextStyle: const TextStyle(color: Colors.purpleAccent), errorTextStyle: const TextStyle(color: Colors.purpleAccent),
boxDecoration: BoxDecoration( boxDecoration: BoxDecoration(
color: Colors.black54, color: Colors.black54,

View File

@ -38,8 +38,26 @@ class _MyHomePageState extends State<MyHomePage> {
CardDetailsValidState? state; CardDetailsValidState? state;
String? errorText; String? errorText;
// Creating a global key here allows us to call the `getStripeResponse()`
// inside the CardTextFieldState widget in our build method. See below
final _key = GlobalKey<CardTextFieldState>();
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final cardField = CardTextField(
key: _key,
loadingWidgetLocation: LoadingLocation.above,
stripePublishableKey: 'pk_test_abc123testmykey',
width: 600,
onValidCardDetails: (details) {
if (kDebugMode) {
print(details);
}
},
overrideValidState: state,
errorText: errorText,
);
return Scaffold( return Scaffold(
body: Center( body: Center(
child: Column( child: Column(
@ -51,22 +69,23 @@ class _MyHomePageState extends State<MyHomePage> {
'Enter your card details below:', 'Enter your card details below:',
), ),
), ),
CardTextField( cardField,
width: 300,
onCardDetailsComplete: (details) {
if (kDebugMode) {
print(details);
}
},
overrideValidState: state,
errorText: errorText,
),
ElevatedButton( ElevatedButton(
child: const Text('Set manual error'), child: const Text('Set manual error'),
onPressed: () => setState(() { onPressed: () => setState(() {
errorText = 'There is a problem'; errorText = 'There is a problem';
state = CardDetailsValidState.invalidCard; state = CardDetailsValidState.invalidCard;
}), }),
),
const SizedBox(height: 12),
ElevatedButton(
child: const Text('Get Stripe token'),
onPressed: () async {
// Here we use the global key to get the stripe data, rather than
// using the `onStripeResponse` callback in the widget
final tok = await _key.currentState?.getStripeResponse();
if (kDebugMode) print(tok);
},
) )
], ],
), ),

View File

@ -206,7 +206,7 @@ packages:
path: ".." path: ".."
relative: true relative: true
source: path source: path
version: "0.0.3" version: "0.0.5"
term_glyph: term_glyph:
dependency: transitive dependency: transitive
description: description:

View File

@ -0,0 +1,19 @@
/// Error class that `CardTextField` throws if any errors are encountered
class CardTextFieldError extends Error {
/// Details provided for the error
String? details;
CardTextFieldErrorType type;
CardTextFieldError(this.type, {this.details});
@override
String toString() {
return 'CardTextFieldError-${type.name}: $details';
}
}
/// Enum to add typing to the `CardTextFieldErrorType`
enum CardTextFieldErrorType {
stripeImplementation,
unknown,
}

View File

@ -2,13 +2,13 @@ library stripe_native_card_field;
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:developer';
import 'dart:io'; import 'dart:io';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:stripe_native_card_field/card_text_field_error.dart';
import 'card_details.dart'; import 'card_details.dart';
import 'card_provider_icon.dart'; import 'card_provider_icon.dart';
@ -17,13 +17,23 @@ import 'card_provider_icon.dart';
/// entry process. /// entry process.
enum CardEntryStep { number, exp, cvc, postal } enum CardEntryStep { number, exp, cvc, postal }
// enum LoadingLocation { ontop, rightInside } enum LoadingLocation { above, below }
/// A uniform text field for entering card details, based /// A uniform text field for entering card details, based
/// on the behavior of Stripe's various html elements. /// on the behavior of Stripe's various html elements.
/// ///
/// Required `width`. /// Required `width`.
/// ///
/// To get the card data or stripe token, provide callbacks
/// for either `onValidCardDetails`, which will return a
/// `CardDetails` object, or `onStripeResponse`, which will
/// return a Map<String, dynamic> response from the Stripe Api.
///
/// If stripe integration is desired, you must provide both
/// `stripePublishableKey` and `onStripeResponse`, otherwise
/// `CardTextField` will `assert(false)` in debug mode or
/// throw a `CardTextFieldError` in profile or release mode
///
/// If the provided `width < 450.0`, the `CardTextField` /// If the provided `width < 450.0`, the `CardTextField`
/// will scroll its content horizontally with the cursor /// will scroll its content horizontally with the cursor
/// to compensate. /// to compensate.
@ -32,7 +42,7 @@ class CardTextField extends StatefulWidget {
Key? key, Key? key,
required this.width, required this.width,
this.onStripeResponse, this.onStripeResponse,
this.onCardDetailsComplete, this.onValidCardDetails,
this.stripePublishableKey, this.stripePublishableKey,
this.height, this.height,
this.textStyle, this.textStyle,
@ -41,8 +51,9 @@ class CardTextField extends StatefulWidget {
this.boxDecoration, this.boxDecoration,
this.errorBoxDecoration, this.errorBoxDecoration,
this.loadingWidget, this.loadingWidget,
this.loadingWidgetLocation = LoadingLocation.below,
this.showInternalLoadingWidget = true, this.showInternalLoadingWidget = true,
this.delayToShowLoading = const Duration(milliseconds: 750), this.delayToShowLoading = const Duration(milliseconds: 0),
this.onCallToStripe, this.onCallToStripe,
this.overrideValidState, this.overrideValidState,
this.errorText, this.errorText,
@ -53,21 +64,17 @@ class CardTextField extends StatefulWidget {
this.iconSize, this.iconSize,
this.cardIconColor, this.cardIconColor,
this.cardIconErrorColor, this.cardIconErrorColor,
// this.loadingWidgetLocation = LoadingLocation.rightInside,
}) : super(key: key) { }) : super(key: key) {
// Setup logic for the CardTextField
// Will assert in debug mode, otherwise will throw `CardTextFieldError` in profile or release
if (stripePublishableKey != null) { if (stripePublishableKey != null) {
assert(stripePublishableKey!.startsWith('pk_')); if (!stripePublishableKey!.startsWith('pk_')) {
if (kReleaseMode && !stripePublishableKey!.startsWith('pk_live_')) { const msg = 'Invalid stripe key, doesn\'t start with "pk_"';
log('StripeNativeCardField: *WARN* You are not using a live publishableKey in production.'); if (kDebugMode) assert(false, msg);
} else if ((kDebugMode || kProfileMode) && if (kReleaseMode || kProfileMode) {
stripePublishableKey!.startsWith('pk_live_')) { throw CardTextFieldError(CardTextFieldErrorType.stripeImplementation,
log('StripeNativeCardField: *WARN* You are using a live stripe key in a debug environment, proceed with caution!'); details: msg);
log('StripeNativeCardField: *WARN* Ideally you should be using your test keys whenever not in production.');
} }
} else {
if (onStripeResponse != null) {
log('StripeNativeCardField: *ERROR* You provided the onTokenReceived callback, but did not provide a stripePublishableKey.');
assert(false);
} }
} }
} }
@ -96,7 +103,8 @@ class CardTextField extends StatefulWidget {
/// Overrides the default box decoration of the text field when there is a validation error /// Overrides the default box decoration of the text field when there is a validation error
final BoxDecoration? errorBoxDecoration; final BoxDecoration? errorBoxDecoration;
/// Shown and overrides CircularProgressIndicator() if the request to stripe takes longer than `delayToShowLoading` /// Shown and overrides `LinearProgressIndicator` if the request to stripe takes longer than `delayToShowLoading`
/// Recommended to only override with a `LinearProgressIndicator` or similar widget, or spacing will be messed up
final Widget? loadingWidget; final Widget? loadingWidget;
/// Overrides default icon size of the card provider, defaults to `Size(30.0, 20.0)` /// Overrides default icon size of the card provider, defaults to `Size(30.0, 20.0)`
@ -109,7 +117,7 @@ class CardTextField extends StatefulWidget {
final String? cardIconErrorColor; final String? cardIconErrorColor;
/// Determines where the loading indicator appears when contacting stripe /// Determines where the loading indicator appears when contacting stripe
// final LoadingLocation loadingWidgetLocation; final LoadingLocation loadingWidgetLocation;
/// Default TextStyle /// Default TextStyle
final TextStyle? textStyle; final TextStyle? textStyle;
@ -122,7 +130,7 @@ class CardTextField extends StatefulWidget {
/// If null, inherits from the `textStyle`. /// If null, inherits from the `textStyle`.
final TextStyle? errorTextStyle; final TextStyle? errorTextStyle;
/// Time to wait until showing the loading indicator when retrieving Stripe token /// Time to wait until showing the loading indicator when retrieving Stripe token, defaults to 0 milliseconds.
final Duration delayToShowLoading; final Duration delayToShowLoading;
/// Whether to show the internal loading widget on calls to Stripe /// Whether to show the internal loading widget on calls to Stripe
@ -138,7 +146,7 @@ class CardTextField extends StatefulWidget {
final void Function(Map<String, dynamic>)? onStripeResponse; final void Function(Map<String, dynamic>)? onStripeResponse;
/// Callback that returns the completed CardDetails object /// Callback that returns the completed CardDetails object
final void Function(CardDetails)? onCardDetailsComplete; final void Function(CardDetails)? onValidCardDetails;
/// Can manually override the ValidState to surface errors returned from Stripe /// Can manually override the ValidState to surface errors returned from Stripe
final CardDetailsValidState? overrideValidState; final CardDetailsValidState? overrideValidState;
@ -146,14 +154,31 @@ class CardTextField extends StatefulWidget {
/// Can manually override the errorText displayed to surface errors returned from Stripe /// Can manually override the errorText displayed to surface errors returned from Stripe
final String? errorText; final String? errorText;
/// GlobalKey used for calling `getStripeToken` in the `CardTextFieldState`
// final GlobalKey<CardTextFieldState> _key = GlobalKey<CardTextFieldState>();
// CardTextFieldState? get state => _key.currentState;
/// Validates the current fields and makes an http request to get the stripe
/// token for the `CardDetails` provided. Will return null if the data is not
/// complete or does not validate properly.
// Future<Map<String, dynamic>?> fetchStripeResponse() async {
// if (kDebugMode && _key.currentState == null) print('Could not fetch Stripe Response, currentState == null');
// return _key.currentState?.getStripeResponse();
// }
@override @override
State<CardTextField> createState() => CardTextFieldState(); State<CardTextField> createState() => CardTextFieldState();
// {
// _state = CardTextFieldState();
// return _state;
// }
} }
/// State Widget for CardTextField /// State Widget for CardTextField
/// Should not be used directly, create a /// Should not be used directly, except to
/// `CardTextField()` instead. /// create a GlobalKey for directly accessing
@visibleForTesting /// the `getStripeResponse` function
class CardTextFieldState extends State<CardTextField> { class CardTextFieldState extends State<CardTextField> {
late TextEditingController _cardNumberController; late TextEditingController _cardNumberController;
late TextEditingController _expirationController; late TextEditingController _expirationController;
@ -373,7 +398,9 @@ class CardTextFieldState extends State<CardTextField> {
} }
}, },
onHorizontalDragEnd: (details) { onHorizontalDragEnd: (details) {
if (!_isMobile || isWideFormat || details.primaryVelocity == null) { if (!_isMobile ||
isWideFormat ||
details.primaryVelocity == null) {
return; return;
} }
@ -399,13 +426,33 @@ class CardTextFieldState extends State<CardTextField> {
child: SizedBox( child: SizedBox(
width: _internalFieldWidth, width: _internalFieldWidth,
height: widget.height ?? 60.0, height: widget.height ?? 60.0,
child: Column(
children: [
if (widget.loadingWidgetLocation ==
LoadingLocation.above)
AnimatedOpacity(
duration: const Duration(milliseconds: 300),
opacity:
_loading && widget.showInternalLoadingWidget
? 1.0
: 0.0,
child: widget.loadingWidget ??
const LinearProgressIndicator(),
),
Padding(
padding: switch (widget.loadingWidgetLocation) {
LoadingLocation.above =>
const EdgeInsets.only(top: 0, bottom: 4.0),
LoadingLocation.below =>
const EdgeInsets.only(top: 4.0, bottom: 0),
},
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
children: [ children: [
Padding( Padding(
padding: padding: const EdgeInsets.symmetric(
const EdgeInsets.symmetric(horizontal: 6.0), horizontal: 6.0),
child: CardProviderIcon( child: CardProviderIcon(
cardDetails: _cardDetails, cardDetails: _cardDetails,
size: widget.iconSize, size: widget.iconSize,
@ -445,9 +492,10 @@ class CardTextFieldState extends State<CardTextField> {
}, },
onChanged: (str) { onChanged: (str) {
final numbers = str.replaceAll(' ', ''); final numbers = str.replaceAll(' ', '');
setState( setState(() =>
() => _cardDetails.cardNumber = numbers); _cardDetails.cardNumber = numbers);
if (str.length <= _cardDetails.maxINNLength) { if (str.length <=
_cardDetails.maxINNLength) {
_cardDetails.detectCardProvider(); _cardDetails.detectCardProvider();
} }
if (numbers.length == 16) { if (numbers.length == 16) {
@ -479,14 +527,16 @@ class CardTextFieldState extends State<CardTextField> {
// fit: _currentStep == CardEntryStep.number ? FlexFit.loose : FlexFit.tight, // fit: _currentStep == CardEntryStep.number ? FlexFit.loose : FlexFit.tight,
child: AnimatedContainer( child: AnimatedContainer(
curve: Curves.easeInOut, curve: Curves.easeInOut,
duration: const Duration(milliseconds: 400), duration:
constraints: const Duration(milliseconds: 400),
_currentStep == CardEntryStep.number constraints: _currentStep ==
CardEntryStep.number
? BoxConstraints.loose( ? BoxConstraints.loose(
Size(_expanderWidthExpanded, 0.0), Size(_expanderWidthExpanded, 0.0),
) )
: BoxConstraints.tight( : BoxConstraints.tight(
Size(_expanderWidthCollapsed, 0.0), Size(
_expanderWidthCollapsed, 0.0),
), ),
), ),
), ),
@ -499,7 +549,8 @@ class CardTextFieldState extends State<CardTextField> {
children: [ children: [
// Must manually add hint label because they wont show on mobile with backspace hack // Must manually add hint label because they wont show on mobile with backspace hack
if (_isMobile && if (_isMobile &&
_expirationController.text == '\u200b') _expirationController.text ==
'\u200b')
Text('MM/YY', style: _hintTextSyle), Text('MM/YY', style: _hintTextSyle),
TextFormField( TextFormField(
key: const Key('expiration_field'), key: const Key('expiration_field'),
@ -517,33 +568,39 @@ class CardTextFieldState extends State<CardTextField> {
validator: (content) { validator: (content) {
if (content == null || if (content == null ||
content.isEmpty || content.isEmpty ||
_isMobile && content == '\u200b') { _isMobile &&
content == '\u200b') {
return null; return null;
} }
if (_isMobile) { if (_isMobile) {
setState(() => setState(() =>
_cardDetails.expirationString = _cardDetails.expirationString =
content.replaceAll('\u200b', '')); content.replaceAll(
'\u200b', ''));
} else { } else {
setState(() => _cardDetails setState(() => _cardDetails
.expirationString = content); .expirationString = content);
} }
if (_cardDetails.validState == if (_cardDetails.validState ==
CardDetailsValidState.dateTooEarly) { CardDetailsValidState
.dateTooEarly) {
_setValidationState( _setValidationState(
'Your card\'s expiration date is in the past.'); 'Your card\'s expiration date is in the past.');
} else if (_cardDetails.validState == } else if (_cardDetails.validState ==
CardDetailsValidState.dateTooLate) { CardDetailsValidState
.dateTooLate) {
_setValidationState( _setValidationState(
'Your card\'s expiration year is invalid.'); 'Your card\'s expiration year is invalid.');
} else if (_cardDetails.validState == } else if (_cardDetails.validState ==
CardDetailsValidState.missingDate) { CardDetailsValidState
.missingDate) {
_setValidationState( _setValidationState(
'You must include your card\'s expiration date.'); 'You must include your card\'s expiration date.');
} else if (_cardDetails.validState == } else if (_cardDetails.validState ==
CardDetailsValidState.invalidMonth) { CardDetailsValidState
.invalidMonth) {
_setValidationState( _setValidationState(
'Your card\'s expiration month is invalid.'); 'Your card\'s expiration month is invalid.');
} }
@ -554,12 +611,12 @@ class CardTextFieldState extends State<CardTextField> {
if (str.isEmpty) { if (str.isEmpty) {
_backspacePressed(); _backspacePressed();
} }
setState(() => setState(() => _cardDetails
_cardDetails.expirationString = .expirationString =
str.replaceAll('\u200b', '')); str.replaceAll('\u200b', ''));
} else { } else {
setState(() => setState(() => _cardDetails
_cardDetails.expirationString = str); .expirationString = str);
} }
if (str.length == 5) { if (str.length == 5) {
_currentCardEntryStepController _currentCardEntryStepController
@ -592,7 +649,8 @@ class CardTextFieldState extends State<CardTextField> {
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
children: [ children: [
if (_isMobile && if (_isMobile &&
_securityCodeController.text == '\u200b') _securityCodeController.text ==
'\u200b')
Text( Text(
'CVC', 'CVC',
style: _hintTextSyle, style: _hintTextSyle,
@ -611,24 +669,29 @@ class CardTextFieldState extends State<CardTextField> {
validator: (content) { validator: (content) {
if (content == null || if (content == null ||
content.isEmpty || content.isEmpty ||
_isMobile && content == '\u200b') { _isMobile &&
content == '\u200b') {
return null; return null;
} }
if (_isMobile) { if (_isMobile) {
setState(() => _cardDetails.securityCode =
content.replaceAll('\u200b', ''));
} else {
setState(() => setState(() =>
_cardDetails.securityCode = content); _cardDetails.securityCode =
content.replaceAll(
'\u200b', ''));
} else {
setState(() => _cardDetails
.securityCode = content);
} }
if (_cardDetails.validState == if (_cardDetails.validState ==
CardDetailsValidState.invalidCVC) { CardDetailsValidState
.invalidCVC) {
_setValidationState( _setValidationState(
'Your card\'s security code is invalid.'); 'Your card\'s security code is invalid.');
} else if (_cardDetails.validState == } else if (_cardDetails.validState ==
CardDetailsValidState.missingCVC) { CardDetailsValidState
.missingCVC) {
_setValidationState( _setValidationState(
'Your card\'s security code is incomplete.'); 'Your card\'s security code is incomplete.');
} }
@ -642,16 +705,17 @@ class CardTextFieldState extends State<CardTextField> {
if (str.isEmpty) { if (str.isEmpty) {
_backspacePressed(); _backspacePressed();
} }
setState(() => setState(() => _cardDetails
_cardDetails.expirationString = .expirationString =
str.replaceAll('\u200b', '')); str.replaceAll('\u200b', ''));
} else { } else {
setState(() => setState(() => _cardDetails
_cardDetails.expirationString = str); .expirationString = str);
} }
if (str.length == if (str.length ==
_cardDetails.provider?.cvcLength) { _cardDetails
.provider?.cvcLength) {
_currentCardEntryStepController _currentCardEntryStepController
.add(CardEntryStep.postal); .add(CardEntryStep.postal);
} }
@ -660,7 +724,8 @@ class CardTextFieldState extends State<CardTextField> {
LengthLimitingTextInputFormatter( LengthLimitingTextInputFormatter(
_cardDetails.provider == null _cardDetails.provider == null
? 4 ? 4
: _cardDetails.provider!.cvcLength), : _cardDetails
.provider!.cvcLength),
FilteringTextInputFormatter.allow( FilteringTextInputFormatter.allow(
RegExp('[0-9]')), RegExp('[0-9]')),
], ],
@ -681,7 +746,8 @@ class CardTextFieldState extends State<CardTextField> {
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
children: [ children: [
if (_isMobile && if (_isMobile &&
_postalCodeController.text == '\u200b') _postalCodeController.text ==
'\u200b')
Text( Text(
'Postal Code', 'Postal Code',
style: _hintTextSyle, style: _hintTextSyle,
@ -700,24 +766,29 @@ class CardTextFieldState extends State<CardTextField> {
validator: (content) { validator: (content) {
if (content == null || if (content == null ||
content.isEmpty || content.isEmpty ||
_isMobile && content == '\u200b') { _isMobile &&
content == '\u200b') {
return null; return null;
} }
if (_isMobile) { if (_isMobile) {
setState(() => _cardDetails.postalCode =
content.replaceAll('\u200b', ''));
} else {
setState(() => setState(() =>
_cardDetails.postalCode = content); _cardDetails.postalCode =
content.replaceAll(
'\u200b', ''));
} else {
setState(() => _cardDetails
.postalCode = content);
} }
if (_cardDetails.validState == if (_cardDetails.validState ==
CardDetailsValidState.invalidZip) { CardDetailsValidState
.invalidZip) {
_setValidationState( _setValidationState(
'The postal code you entered is not correct.'); 'The postal code you entered is not correct.');
} else if (_cardDetails.validState == } else if (_cardDetails.validState ==
CardDetailsValidState.missingZip) { CardDetailsValidState
.missingZip) {
_setValidationState( _setValidationState(
'You must enter your card\'s postal code.'); 'You must enter your card\'s postal code.');
} }
@ -728,11 +799,12 @@ class CardTextFieldState extends State<CardTextField> {
if (str.isEmpty) { if (str.isEmpty) {
_backspacePressed(); _backspacePressed();
} }
setState(() => _cardDetails.postalCode = setState(() => _cardDetails
.postalCode =
str.replaceAll('\u200b', '')); str.replaceAll('\u200b', ''));
} else { } else {
setState( setState(() =>
() => _cardDetails.postalCode = str); _cardDetails.postalCode = str);
} }
}, },
textInputAction: TextInputAction.done, textInputAction: TextInputAction.done,
@ -741,7 +813,8 @@ class CardTextFieldState extends State<CardTextField> {
}, },
decoration: InputDecoration( decoration: InputDecoration(
contentPadding: EdgeInsets.zero, contentPadding: EdgeInsets.zero,
hintText: _isMobile ? '' : 'Postal Code', hintText:
_isMobile ? '' : 'Postal Code',
hintStyle: _hintTextSyle, hintStyle: _hintTextSyle,
fillColor: Colors.transparent, fillColor: Colors.transparent,
border: InputBorder.none, border: InputBorder.none,
@ -750,6 +823,11 @@ class CardTextFieldState extends State<CardTextField> {
], ],
), ),
), ),
],
),
),
if (widget.loadingWidgetLocation ==
LoadingLocation.below)
AnimatedOpacity( AnimatedOpacity(
duration: const Duration(milliseconds: 300), duration: const Duration(milliseconds: 300),
opacity: opacity:
@ -757,7 +835,7 @@ class CardTextFieldState extends State<CardTextField> {
? 1.0 ? 1.0
: 0.0, : 0.0,
child: widget.loadingWidget ?? child: widget.loadingWidget ??
const CircularProgressIndicator(), const LinearProgressIndicator(),
), ),
], ],
), ),
@ -784,22 +862,31 @@ class CardTextFieldState extends State<CardTextField> {
); );
} }
Future<void> _postalFieldSubmitted() async { // Makes an http call to stripe API with provided card credentials and returns the result
Future<Map<String, dynamic>?> getStripeResponse() async {
_validateFields(); _validateFields();
if (_cardDetails.isComplete) {
if (widget.onCardDetailsComplete != null) {
widget.onCardDetailsComplete!(_cardDetails);
} else if (widget.onStripeResponse != null) {
bool returned = false;
if (!_cardDetails.isComplete) {
if (kDebugMode)
print(
'Could not get stripe response, card details not complete: $_cardDetails');
return null;
}
if (widget.onCallToStripe != null) widget.onCallToStripe!();
if (widget.stripePublishableKey == null) {
if (kDebugMode)
print(
'***ERROR tried calling `getStripeToken()` but no stripe key provided');
return null;
}
bool returned = false;
Future.delayed( Future.delayed(
const Duration(milliseconds: 750), widget.delayToShowLoading,
() => returned ? null : setState(() => _loading = true), () => returned ? null : setState(() => _loading = true),
); );
const stripeCardUrl = 'https://api.stripe.com/v1/tokens'; const stripeCardUrl = 'https://api.stripe.com/v1/tokens';
// Callback that stripe call is being made
if (widget.onCallToStripe != null) widget.onCallToStripe!();
final response = await http.post( final response = await http.post(
Uri.parse(stripeCardUrl), Uri.parse(stripeCardUrl),
body: { body: {
@ -814,9 +901,22 @@ class CardTextFieldState extends State<CardTextField> {
); );
returned = true; returned = true;
final jsonBody = jsonDecode(response.body); final Map<String, dynamic> jsonBody = jsonDecode(response.body);
if (_loading) setState(() => _loading = false);
return jsonBody;
}
widget.onStripeResponse!(jsonBody); Future<void> _postalFieldSubmitted() async {
_validateFields();
if (_cardDetails.isComplete) {
if (widget.onValidCardDetails != null) {
widget.onValidCardDetails!(_cardDetails);
} else if (widget.onStripeResponse != null) {
// Callback that stripe call is being made
if (widget.onCallToStripe != null) widget.onCallToStripe!();
final jsonBody = await getStripeResponse();
if (jsonBody != null) widget.onStripeResponse!(jsonBody);
if (_loading) setState(() => _loading = false); if (_loading) setState(() => _loading = false);
} }
} }

View File

@ -1,6 +1,6 @@
name: stripe_native_card_field name: stripe_native_card_field
description: A native flutter implementation of the elegant Stripe Card Field. description: A native flutter implementation of the elegant Stripe Card Field.
version: 0.0.5 version: 0.0.6
repository: https://git.fosscat.com/n8r/stripe_native_card_field repository: https://git.fosscat.com/n8r/stripe_native_card_field
environment: environment:

View File

@ -18,7 +18,7 @@ void main() {
CardDetails? details; CardDetails? details;
final cardField = CardTextField( final cardField = CardTextField(
width: width, width: width,
onCardDetailsComplete: (cd) => details = cd, onValidCardDetails: (cd) => details = cd,
); );
await tester.pumpWidget(baseCardFieldWidget(cardField)); await tester.pumpWidget(baseCardFieldWidget(cardField));
@ -110,7 +110,7 @@ void main() {
final cardField = CardTextField( final cardField = CardTextField(
width: width, width: width,
onCardDetailsComplete: (cd) => details = cd, onValidCardDetails: (cd) => details = cd,
); );
await tester.pumpWidget(baseCardFieldWidget(cardField)); await tester.pumpWidget(baseCardFieldWidget(cardField));