diff --git a/CHANGELOG.md b/CHANGELOG.md index 8508ed6..7ad64c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## 2.2.7 -04/04/2022 +- Option to listen for multiple sms on android +| Property | Meaning/Default | +|------------|:-------:| +| listenForMultipleSmsOnAndroid | false | + + ## 2.2.6 -02/04/2022 - Updated smart_auth - Updated readme diff --git a/README.md b/README.md index 66a313b..810b86c 100644 --- a/README.md +++ b/README.md @@ -59,13 +59,13 @@ Don't forget to give it a star ⭐ ## Demo -| [Live Demo](https://rebrand.ly/6390b8) | Rounded With Shadows | Rounded With Cursor | -| --------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | -| ![Live Demo](https://user-images.githubusercontent.com/26390946/155666045-aa93bf48-f8e7-407c-bb19-bc247d9e12bd.png) | ![Rounded With Shadows](https://user-images.githubusercontent.com/26390946/155599527-fe934f2c-5124-4754-bbf6-bb97d55a77c0.gif) | ![Rounded With Cursor](https://user-images.githubusercontent.com/26390946/155599870-03387689-7be2-4a30-8e6f-90136a0515be.gif) | +| [Live Demo](https://rebrand.ly/6390b8) | Rounded With Shadows | Rounded With Cursor | +| - | - | - | +| [![Live Demo](https://user-images.githubusercontent.com/26390946/155666045-aa93bf48-f8e7-407c-bb19-bc247d9e12bd.png)](https://rebrand.ly/6390b8) | ![Rounded With Shadows](https://user-images.githubusercontent.com/26390946/155599527-fe934f2c-5124-4754-bbf6-bb97d55a77c0.gif) | ![Rounded With Cursor](https://user-images.githubusercontent.com/26390946/155599870-03387689-7be2-4a30-8e6f-90136a0515be.gif) | -| Rounded Filled | With Bottom Cursor | Filled | -| -------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------ | +| Rounded Filled | With Bottom Cursor | Filled | +| - | - | - | | ![Rounded Filled](https://user-images.githubusercontent.com/26390946/155600099-d0a02f55-09e6-4142-92de-066cd71cf211.gif) | ![With Bottom Cursor](https://user-images.githubusercontent.com/26390946/155600276-0380b3b4-3d9c-4ea8-87d0-4f7ebd86e460.gif) | ![Filled](https://user-images.githubusercontent.com/26390946/155600427-901c1eae-e565-4cf8-a338-8ac40eb1149c.gif) | ## Getting Started @@ -87,78 +87,78 @@ Pin smoothly animates from one state to another automatically. You can use standard Pinput like so ```dart - Widget buildPinPut() { - return Pinput( - onCompleted: (pin) => print(pin), - ); - } + Widget buildPinPut() { + return Pinput( + onCompleted: (pin) => print(pin), + ); + } ``` If you want to customize it, create `defaultPinTheme` first. ```dart - final defaultPinTheme = PinTheme( - width: 56, - height: 56, - textStyle: TextStyle(fontSize: 20, color: Color.fromRGBO(30, 60, 87, 1), fontWeight: FontWeight.w600), - decoration: BoxDecoration( - border: Border.all(color: Color.fromRGBO(234, 239, 243, 1)), - borderRadius: BorderRadius.circular(20), - ), - ); + final defaultPinTheme = PinTheme( + width: 56, + height: 56, + textStyle: TextStyle(fontSize: 20, color: Color.fromRGBO(30, 60, 87, 1), fontWeight: FontWeight.w600), + decoration: BoxDecoration( + border: Border.all(color: Color.fromRGBO(234, 239, 243, 1)), + borderRadius: BorderRadius.circular(20), + ), + ); ``` if you want all pins to be the same don't pass other theme parameters, If not, create `focusedPinTheme`, `submittedPinTheme`, `followingPinTheme`, `errorPinTheme` from `defaultPinTheme` ```dart - final focusedPinTheme = defaultPinTheme.copyDecorationWith( - border: Border.all(color: Color.fromRGBO(114, 178, 238, 1)), - borderRadius: BorderRadius.circular(8), - ); - - final submittedPinTheme = defaultPinTheme.copyWith( - decoration: defaultPinTheme.decoration.copyWith( - color: Color.fromRGBO(234, 239, 243, 1), - ), - ); + final focusedPinTheme = defaultPinTheme.copyDecorationWith( + border: Border.all(color: Color.fromRGBO(114, 178, 238, 1)), + borderRadius: BorderRadius.circular(8), + ); + + final submittedPinTheme = defaultPinTheme.copyWith( + decoration: defaultPinTheme.decoration.copyWith( + color: Color.fromRGBO(234, 239, 243, 1), + ), + ); ``` Put everything together ```dart - final defaultPinTheme = PinTheme( - width: 56, - height: 56, - textStyle: TextStyle(fontSize: 20, color: Color.fromRGBO(30, 60, 87, 1), fontWeight: FontWeight.w600), - decoration: BoxDecoration( - border: Border.all(color: Color.fromRGBO(234, 239, 243, 1)), - borderRadius: BorderRadius.circular(20), - ), - ); - - final focusedPinTheme = defaultPinTheme.copyDecorationWith( - border: Border.all(color: Color.fromRGBO(114, 178, 238, 1)), - borderRadius: BorderRadius.circular(8), - ); - - final submittedPinTheme = defaultPinTheme.copyWith( - decoration: defaultPinTheme.decoration.copyWith( - color: Color.fromRGBO(234, 239, 243, 1), - ), - ); - - return Pinput( - defaultPinTheme: defaultPinTheme, - focusedPinTheme: focusedPinTheme, - submittedPinTheme: submittedPinTheme, - validator: (s) { - return s == '2222' ? null : 'Pin is incorrect'; - }, - pinputAutovalidateMode: PinputAutovalidateMode.onSubmit, - showCursor: true, - onCompleted: (pin) => print(pin), - ); + final defaultPinTheme = PinTheme( + width: 56, + height: 56, + textStyle: TextStyle(fontSize: 20, color: Color.fromRGBO(30, 60, 87, 1), fontWeight: FontWeight.w600), + decoration: BoxDecoration( + border: Border.all(color: Color.fromRGBO(234, 239, 243, 1)), + borderRadius: BorderRadius.circular(20), + ), + ); + + final focusedPinTheme = defaultPinTheme.copyDecorationWith( + border: Border.all(color: Color.fromRGBO(114, 178, 238, 1)), + borderRadius: BorderRadius.circular(8), + ); + + final submittedPinTheme = defaultPinTheme.copyWith( + decoration: defaultPinTheme.decoration.copyWith( + color: Color.fromRGBO(234, 239, 243, 1), + ), + ); + + return Pinput( + defaultPinTheme: defaultPinTheme, + focusedPinTheme: focusedPinTheme, + submittedPinTheme: submittedPinTheme, + validator: (s) { + return s == '2222' ? null : 'Pin is incorrect'; + }, + pinputAutovalidateMode: PinputAutovalidateMode.onSubmit, + showCursor: true, + onCompleted: (pin) => print(pin), + ); ``` ## SMS Autofill @@ -169,6 +169,9 @@ Works out of the box, by tapping the code on top of the keyboard ### Android +`If you are using` [firebase_auth](https://firebase.flutter.dev/docs/auth/phone#verificationcompleted) `you don't need to use any option below, +because firebase_auth support automatic SMS code resolution.` + You have two options, [SMS Retriever API](https://developers.google.com/identity/sms-retriever/overview?hl=en) and [SMS User Consent API](https://developers.google.com/identity/sms-retriever/user-consent/overview), [SmartAuth](https://github.com/Tkko/flutter_smart_auth) is a wrapper package for Flutter for these APIs and it is behind the autofill support of Pinput @@ -289,193 +292,220 @@ Also, you can suggest a phone number by showing native Android dialog. ## Properties ```dart - /// Theme of the pin in default state - final PinTheme? defaultPinTheme; - - /// Theme of the pin in focused state - final PinTheme? focusedPinTheme; - - /// Theme of the pin in submitted state - final PinTheme? submittedPinTheme; - - /// Theme of the pin in following state - final PinTheme? followingPinTheme; - - /// Theme of the pin in disabled state - final PinTheme? disabledPinTheme; - - /// Theme of the pin in error state - final PinTheme? errorPinTheme; - - /// If true keyboard will be closed - final bool closeKeyboardWhenCompleted; - - /// Displayed fields count. PIN code length. - final int length; - - /// Fires when user completes pin input - final ValueChanged? onCompleted; - - /// Called every time input value changes. - final ValueChanged? onChanged; - - /// See [EditableText.onSubmitted] - final ValueChanged? onSubmitted; - - /// Called when user clicks on PinPut - final VoidCallback? onTap; - - /// In order to catch event [enableInteractiveSelection] should be false - final VoidCallback? onLongPress; - - /// Used to get, modify PinPut value and more. - /// Don't forget to dispose controller - /// ``` dart - /// @override - /// void dispose() { - /// controller.dispose(); - /// super.dispose(); - /// } - /// ``` - final TextEditingController? controller; - - /// Defines the keyboard focus for this - /// To give the keyboard focus to this widget, provide a [focusNode] and then - /// use the current [FocusScope] to request the focus: - /// Don't forget to dispose focusNode - /// ``` dart - /// @override - /// void dispose() { - /// focusNode.dispose(); - /// super.dispose(); - /// } - /// ``` - final FocusNode? focusNode; - - /// Widget that is displayed before field submitted. - final Widget? preFilledWidget; - - /// Sets the positions where the separator should be shown - final List? separatorPositions; - - /// Builds a Pinput separator - final Widget? separator; - - /// Defines how [Pinput] fields are being placed inside [Row] - final MainAxisAlignment mainAxisAlignment; - - /// Defines how each [Pinput] field are being placed within the container - final AlignmentGeometry pinContentAlignment; - - /// curve of every [Pinput] Animation - final Curve animationCurve; - - /// Duration of every [Pinput] Animation - final Duration animationDuration; - - /// Animation Type of each [Pinput] field - /// options: - /// none, scale, fade, slide, rotation - final PinAnimationType pinAnimationType; - - /// Begin Offset of ever [Pinput] field when [pinAnimationType] is slide - final Offset? slideTransitionBeginOffset; - - /// Defines [Pinput] state - final bool enabled; - - /// See [EditableText.readOnly] - final bool readOnly; - - /// See [EditableText.autofocus] - final bool autofocus; - - /// Whether to use Native keyboard or custom one - /// when flag is set to false [Pinput] wont be focusable anymore - /// so you should set value of [Pinput]'s [TextEditingController] programmatically - final bool useNativeKeyboard; - - /// If true, paste button will appear on longPress event - final bool toolbarEnabled; - - /// Whether show cursor or not - /// Default cursor '|' or [cursor] - final bool showCursor; - - /// If [showCursor] true the focused field will show passed Widget - final Widget? cursor; - - /// The appearance of the keyboard. - /// This setting is only honored on iOS devices. - /// If unset, defaults to [ThemeData.brightness]. - final Brightness? keyboardAppearance; - - /// See [EditableText.inputFormatters] - final List inputFormatters; - - /// See [EditableText.keyboardType] - final TextInputType keyboardType; - - /// Provide any symbol to obscure each [Pinput] pin - /// Recommended ● - final String obscuringCharacter; - - /// IF [obscureText] is true typed text will be replaced with passed Widget - final Widget? obscuringWidget; - - /// Whether hide typed pin or not - final bool obscureText; - - /// See [EditableText.textCapitalization] - final TextCapitalization textCapitalization; - - /// The type of action button to use for the keyboard. - /// - /// Defaults to [TextInputAction.newline] if [keyboardType] is - /// [TextInputType.multiline] and [TextInputAction.done] otherwise. - final TextInputAction? textInputAction; - - /// Configuration of toolbar options. - /// - /// If not set, select all and paste will default to be enabled. Copy and cut - /// will be disabled if [obscureText] is true. If [readOnly] is true, - /// paste and cut will be disabled regardless. - final ToolbarOptions toolbarOptions; - - /// See [EditableText.autofillHints] - final Iterable? autofillHints; - - /// See [EditableText.enableSuggestions] - final bool enableSuggestions; - - /// See [EditableText.selectionControls] - final TextSelectionControls? selectionControls; - - /// See [TextField.restorationId] - final String? restorationId; - - /// Fires when clipboard has text of Pinput's length - final ValueChanged? onClipboardFound; - - /// Use haptic feedback everytime user types on keyboard - /// See more details in [HapticFeedback] - final HapticFeedbackType hapticFeedbackType; - - /// See [EditableText.onAppPrivateCommand] - final AppPrivateCommandCallback? onAppPrivateCommand; - - /// See [EditableText.mouseCursor] - final MouseCursor? mouseCursor; - - /// Style of error text - final TextStyle? errorTextStyle; - - /// If [showError] is true and [errorBuilder] is passed it will be rendered under the Pinput - final PinputErrorBuilder? errorBuilder; - - /// Return null if pin is valid or any String otherwise - final FormFieldValidator? validator; - - /// Return null if pin is valid or any String otherwise - final PinputAutovalidateMode pinputAutovalidateMode; + /// Theme of the pin in default state + final PinTheme? defaultPinTheme; + + /// Theme of the pin in focused state + final PinTheme? focusedPinTheme; + + /// Theme of the pin in submitted state + final PinTheme? submittedPinTheme; + + /// Theme of the pin in following state + final PinTheme? followingPinTheme; + + /// Theme of the pin in disabled state + final PinTheme? disabledPinTheme; + + /// Theme of the pin in error state + final PinTheme? errorPinTheme; + + /// If true keyboard will be closed + final bool closeKeyboardWhenCompleted; + + /// Displayed fields count. PIN code length. + final int length; + + /// By default Android autofill is Disabled, you cane enable it by using any of options listed below + /// + /// First option is [AndroidSmsAutofillMethod.smsRetrieverApi] it automatically reads sms without user interaction + /// More about Sms Retriever API [https://developers.google.com/identity/sms-retriever/overview?hl=en] + /// + /// Second option requires user interaction to confirm reading a SMS, See readme for more details + /// [AndroidSmsAutofillMethod.smsUserConsentApi] + /// More about SMS User Consent API [https://developers.google.com/identity/sms-retriever/user-consent/overview] + final AndroidSmsAutofillMethod androidSmsAutofillMethod; + + /// If true [androidSmsAutofillMethod] is not [AndroidSmsAutofillMethod.none] + /// Pinput will listen multiple sms codes, helpful if user request another sms code + final bool listenForMultipleSmsOnAndroid; + + /// Used to extract code from SMS for Android Autofill if [androidSmsAutofillMethod] is enabled + /// By default it is [_defaultSmsCodeMatcher] + final String? smsCodeMatcher; + + /// Fires when user completes pin input + final ValueChanged? onCompleted; + + /// Called every time input value changes. + final ValueChanged? onChanged; + + /// See [EditableText.onSubmitted] + final ValueChanged? onSubmitted; + + /// Called when user clicks on PinPut + final VoidCallback? onTap; + + /// In order to catch event [enableInteractiveSelection] should be false + final VoidCallback? onLongPress; + + /// Used to get, modify PinPut value and more. + /// Don't forget to dispose controller + /// ``` dart + /// @override + /// void dispose() { + /// controller.dispose(); + /// super.dispose(); + /// } + /// ``` + final TextEditingController? controller; + + /// Defines the keyboard focus for this + /// To give the keyboard focus to this widget, provide a [focusNode] and then + /// use the current [FocusScope] to request the focus: + /// Don't forget to dispose focusNode + /// ``` dart + /// @override + /// void dispose() { + /// focusNode.dispose(); + /// super.dispose(); + /// } + /// ``` + final FocusNode? focusNode; + + /// Widget that is displayed before field submitted. + final Widget? preFilledWidget; + + /// Sets the positions where the separator should be shown + final List? separatorPositions; + + /// Builds a Pinput separator + final Widget? separator; + + /// Defines how [Pinput] fields are being placed inside [Row] + final MainAxisAlignment mainAxisAlignment; + + /// Defines how [Pinput] and ([errorText] or [errorBuilder]) are being placed inside [Column] + final CrossAxisAlignment crossAxisAlignment; + + /// Defines how each [Pinput] field are being placed within the container + final AlignmentGeometry pinContentAlignment; + + /// curve of every [Pinput] Animation + final Curve animationCurve; + + /// Duration of every [Pinput] Animation + final Duration animationDuration; + + /// Animation Type of each [Pinput] field + /// options: + /// none, scale, fade, slide, rotation + final PinAnimationType pinAnimationType; + + /// Begin Offset of ever [Pinput] field when [pinAnimationType] is slide + final Offset? slideTransitionBeginOffset; + + /// Defines [Pinput] state + final bool enabled; + + /// See [EditableText.readOnly] + final bool readOnly; + + /// See [EditableText.autofocus] + final bool autofocus; + + /// Whether to use Native keyboard or custom one + /// when flag is set to false [Pinput] wont be focusable anymore + /// so you should set value of [Pinput]'s [TextEditingController] programmatically + final bool useNativeKeyboard; + + /// If true, paste button will appear on longPress event + final bool toolbarEnabled; + + /// Whether show cursor or not + /// Default cursor '|' or [cursor] + final bool showCursor; + + /// If [showCursor] true the focused field will show passed Widget + final Widget? cursor; + + /// The appearance of the keyboard. + /// This setting is only honored on iOS devices. + /// If unset, defaults to [ThemeData.brightness]. + final Brightness? keyboardAppearance; + + /// See [EditableText.inputFormatters] + final List inputFormatters; + + /// See [EditableText.keyboardType] + final TextInputType keyboardType; + + /// Provide any symbol to obscure each [Pinput] pin + /// Recommended ● + final String obscuringCharacter; + + /// IF [obscureText] is true typed text will be replaced with passed Widget + final Widget? obscuringWidget; + + /// Whether hide typed pin or not + final bool obscureText; + + /// See [EditableText.textCapitalization] + final TextCapitalization textCapitalization; + + /// The type of action button to use for the keyboard. + /// + /// Defaults to [TextInputAction.newline] if [keyboardType] is + /// [TextInputType.multiline] and [TextInputAction.done] otherwise. + final TextInputAction? textInputAction; + + /// Configuration of toolbar options. + /// + /// If not set, select all and paste will default to be enabled. Copy and cut + /// will be disabled if [obscureText] is true. If [readOnly] is true, + /// paste and cut will be disabled regardless. + final ToolbarOptions toolbarOptions; + + /// See [EditableText.autofillHints] + final Iterable? autofillHints; + + /// See [EditableText.enableSuggestions] + final bool enableSuggestions; + + /// See [EditableText.selectionControls] + final TextSelectionControls? selectionControls; + + /// See [TextField.restorationId] + final String? restorationId; + + /// Fires when clipboard has text of Pinput's length + final ValueChanged? onClipboardFound; + + /// Use haptic feedback everytime user types on keyboard + /// See more details in [HapticFeedback] + final HapticFeedbackType hapticFeedbackType; + + /// See [EditableText.onAppPrivateCommand] + final AppPrivateCommandCallback? onAppPrivateCommand; + + /// See [EditableText.mouseCursor] + final MouseCursor? mouseCursor; + + /// If true [errorPinTheme] will be applied and [errorText] will be displayed under the Pinput + final bool forceErrorState; + + /// Text displayed under the Pinput if Pinput is invalid + final String? errorText; + + /// Style of error text + final TextStyle? errorTextStyle; + + /// If [showError] is true and [errorBuilder] is passed it will be rendered under the Pinput + final PinputErrorBuilder? errorBuilder; + + /// Return null if pin is valid or any String otherwise + final FormFieldValidator? validator; + + /// Return null if pin is valid or any String otherwise + final PinputAutovalidateMode pinputAutovalidateMode; ``` diff --git a/example/lib/pinput_templates/rounded_with_cursor.dart b/example/lib/pinput_templates/rounded_with_cursor.dart index d2ef030..de1ac92 100644 --- a/example/lib/pinput_templates/rounded_with_cursor.dart +++ b/example/lib/pinput_templates/rounded_with_cursor.dart @@ -45,6 +45,7 @@ class _RoundedWithCustomCursorState extends State { controller: pinController, focusNode: focusNode, androidSmsAutofillMethod: AndroidSmsAutofillMethod.smsRetrieverApi, + listenForMultipleSmsOnAndroid: true, defaultPinTheme: defaultPinTheme, validator: (value) { return value == '2222' ? null : 'Pin is incorrect'; diff --git a/lib/src/pinput.dart b/lib/src/pinput.dart index 02c8869..3a2a256 100644 --- a/lib/src/pinput.dart +++ b/lib/src/pinput.dart @@ -46,6 +46,7 @@ class Pinput extends StatefulWidget { this.separator = _defaultSeparator, this.smsCodeMatcher = _defaultSmsCodeMatcher, this.androidSmsAutofillMethod = AndroidSmsAutofillMethod.none, + this.listenForMultipleSmsOnAndroid = false, this.mainAxisAlignment = MainAxisAlignment.center, this.crossAxisAlignment = CrossAxisAlignment.start, this.pinContentAlignment = Alignment.center, @@ -125,6 +126,10 @@ class Pinput extends StatefulWidget { /// More about SMS User Consent API [https://developers.google.com/identity/sms-retriever/user-consent/overview] final AndroidSmsAutofillMethod androidSmsAutofillMethod; + /// If true [androidSmsAutofillMethod] is not [AndroidSmsAutofillMethod.none] + /// Pinput will listen multiple sms codes, helpful if user request another sms code + final bool listenForMultipleSmsOnAndroid; + /// Used to extract code from SMS for Android Autofill if [androidSmsAutofillMethod] is enabled /// By default it is [_defaultSmsCodeMatcher] final String? smsCodeMatcher; diff --git a/lib/src/pinput_state.dart b/lib/src/pinput_state.dart index 5eed401..b98bbe2 100644 --- a/lib/src/pinput_state.dart +++ b/lib/src/pinput_state.dart @@ -89,18 +89,30 @@ class _PinputState extends State if (isAndroid && isAutofillEnabled) { _smartAuth = SmartAuth(); - if (widget.androidSmsAutofillMethod == - AndroidSmsAutofillMethod.smsRetrieverApi) { - _smartAuth!.getAppSignature().then((value) => debugPrint( - 'Pinput: App Signature for SMS Retriever API Is: $value')); - } - final useUserConsentApi = widget.androidSmsAutofillMethod == - AndroidSmsAutofillMethod.smsUserConsentApi; - final res = - await _smartAuth!.getSmsCode(useUserConsentApi: useUserConsentApi); - if (res.succeed && res.code?.length == widget.length) { - _effectiveController.setText(res.code!); - } + _maybePrintAppSignature(); + _listenForSmsCode(); + } + } + + void _maybePrintAppSignature() async { + if (widget.androidSmsAutofillMethod == + AndroidSmsAutofillMethod.smsRetrieverApi) { + final res = await _smartAuth!.getAppSignature(); + debugPrint('Pinput: App Signature for SMS Retriever API Is: $res'); + } + } + + void _listenForSmsCode() async { + final useUserConsentApi = widget.androidSmsAutofillMethod == + AndroidSmsAutofillMethod.smsUserConsentApi; + final res = + await _smartAuth!.getSmsCode(useUserConsentApi: useUserConsentApi); + if (res.succeed && res.codeFound && res.code!.length == widget.length) { + _effectiveController.setText(res.code!); + } + // Listen for multiple sms codes + if (widget.listenForMultipleSmsOnAndroid) { + _listenForSmsCode(); } } diff --git a/pubspec.yaml b/pubspec.yaml index 92740b8..b34dc27 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: pinput description: Pin code input (OTP) text field, iOS SMS autofill, Android SMS autofill One Time Code, Password, Passcode, Captcha, Security, Coupon, Wowcher, 2FA, Two step verification homepage: https://github.com/Tkko/Flutter_PinPut repository: https://github.com/Tkko/Flutter_PinPut -version: 2.2.6 +version: 2.2.7 environment: sdk: '>=2.12.0 <3.0.0'