0.0.6 release
This commit is contained in:
parent
d7d27a1cf5
commit
462e40308f
10
CHANGELOG.md
10
CHANGELOG.md
|
@ -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`
|
||||||
|
|
21
README.md
21
README.md
|
@ -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.
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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);
|
||||||
|
},
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
|
@ -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:
|
||||||
|
|
19
lib/card_text_field_error.dart
Normal file
19
lib/card_text_field_error.dart
Normal 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,
|
||||||
|
}
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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));
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user