vgs-samples / vgs-collect-show-flutter-demo

VGS Collect & Show SDKs - Flutter Demo
https://www.verygoodsecurity.com/
4 stars 6 forks source link

VGSCardNumberEditText and CardVerificationCodeEditText not allowing keyboard input In Android #1

Closed akimau closed 2 years ago

akimau commented 2 years ago

The above issue happens for Android not iOS. It occurs when you have a previous page that has textfields and a user enters data into those fields via the keyboard before navigating to the cards collect view. For example if you have a page that has textfields for capturing shipping details, then navigate to the cards collect view page to add a card, the VGSCardNumberEditText and CardVerificationCodeEditText don't allow input from the keyboard. Not sure why this happens, could be a bug. The example in this repo cannot show the issue unless you add a page before the MyHomePage that has a textfield e.g I added a HelloWorldPage to the example that is shown before the MyHomePage to demonstrate the issue

import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: HelloWorldPage(),
    );
  }
}

class HelloWorldPage extends StatefulWidget {
  @override
  State<HelloWorldPage> createState() => _HelloWorldPageState();
}

class _HelloWorldPageState extends State<HelloWorldPage> {
  final _amountTextController = TextEditingController(text: "1");
  final _refNumberTextController = TextEditingController(text: "1234");

  final _amountFocusNode = FocusNode();
  final _refNumberFocusNode = FocusNode();

  @override
  void dispose() {
    _amountTextController.dispose();
    _refNumberTextController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Hello World Page"),
      ),
      body: SafeArea(
        child: Column(
          children: [
            TextField(
              focusNode: _amountFocusNode,
              controller: _amountTextController,
              decoration: InputDecoration(
                hintText: "Enter amount to pay",
              ),
              keyboardType: TextInputType.number,
              textInputAction: TextInputAction.next,
              onSubmitted: (_) {
                _amountFocusNode.unfocus();
                _refNumberFocusNode.requestFocus();
              },
            ),
            SizedBox(height: 20.0),
            TextField(
              focusNode: _refNumberFocusNode,
              controller: _refNumberTextController,
              decoration: InputDecoration(
                hintText: "Enter reference number",
              ),
              textInputAction: TextInputAction.done,
              onSubmitted: (_) {
                _refNumberFocusNode.unfocus();
              },
            ),
            SizedBox(height: 40.0),
            ElevatedButton(
              style: ElevatedButton.styleFrom(
                primary: Colors.red,
                shape: RoundedRectangleBorder(
                  borderRadius: BorderRadius.circular(0.0),
                ),
                padding: EdgeInsets.symmetric(vertical: 16.0),
              ),
              onPressed: () {
                Navigator.push(
                  context,
                  MaterialPageRoute(
                    builder: (_) => MyHomePage(
                      title: 'Flutter with VGS Collect/Show SDK',
                    ),
                  ),
                );
              },
              child: Text("Click Me"),
            ),
          ],
        ),
      ),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

const COLLECT_FORM_VIEW_TYPE = 'card-collect-form-view';
const SHOW_FORM_VIEW_TYPE = 'card-show-form-view';

const CARD_TOKEN_KEY = 'cardNumber';
const DATE_TOKEN_KEY = 'expDate';

const COLLECT_ERROR_CODE_KEY = 'collect_error_code';
const COLLECT_ERROR_MESSAGE_KEY = 'collect_error_message';

const SHOW_SUCCESS_CODE_KEY = 'show_status_code';
const SHOW_ERROR_CODE_KEY = 'show_error_code';
const SHOW_ERROR_MESSAGE_KEY = 'show_error_message';

class _MyHomePageState extends State<MyHomePage> {
  CardCollectFormController collectController;
  CardShowFormController showController;

  String cardToken;
  String dateToken;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
        backgroundColor: Color(0xff3c4c5d),
      ),
      body: Row(children: <Widget>[
        Expanded(
          child: Padding(
            padding: const EdgeInsets.only(
                left: 12.0, top: 12.0, right: 6.0, bottom: 12.0),
            child: Column(children: <Widget>[
              Expanded(child: Builder(builder: (context) {
                // any logic needed...
                final isAndorid =
                    defaultTargetPlatform == TargetPlatform.android;

                return isAndorid ? _cardCollect() : _cardCollectNativeiOS();
              })),
              Padding(
                padding: const EdgeInsets.only(bottom: 12.0),
                child: _collectButton(),
              ),
              Text("Powered by VGS Collect SDK",
                  style:
                      TextStyle(fontSize: 10.0, fontWeight: FontWeight.bold)),
            ]),
          ),
        ),
        VerticalDivider(
          color: Color(0xff3c4c5d),
        ),
        Expanded(
          child: Padding(
            padding: const EdgeInsets.only(
                left: 6.0, top: 16.0, right: 12.0, bottom: 12.0),
            child: Column(children: <Widget>[
              Expanded(child: Builder(builder: (context) {
                // any logic needed...
                final isAndorid =
                    defaultTargetPlatform == TargetPlatform.android;

                return isAndorid ? _cardShow() : _cardShowNativeiOS();
              })),
              Padding(
                padding: const EdgeInsets.only(bottom: 12.0),
                child: _showButton(),
              ),
              Text("Powered by VGS Show SDK",
                  style:
                      TextStyle(fontSize: 10.0, fontWeight: FontWeight.bold)),
            ]),
          ),
        ),
      ]),
    );
  }

  Widget _cardCollect() {
    final Map<String, dynamic> creationParams = <String, dynamic>{};
    return PlatformViewLink(
      viewType: COLLECT_FORM_VIEW_TYPE,
      surfaceFactory:
          (BuildContext context, PlatformViewController controller) {
        return AndroidViewSurface(
          controller: controller,
          gestureRecognizers: const <Factory<OneSequenceGestureRecognizer>>{},
          hitTestBehavior: PlatformViewHitTestBehavior.opaque,
        );
      },
      onCreatePlatformView: (PlatformViewCreationParams params) {
        var platformView = PlatformViewsService.initSurfaceAndroidView(
          id: params.id,
          viewType: COLLECT_FORM_VIEW_TYPE,
          layoutDirection: TextDirection.ltr,
          creationParams: creationParams,
          creationParamsCodec: StandardMessageCodec(),
        );
        platformView
            .addOnPlatformViewCreatedListener(params.onPlatformViewCreated);
        platformView
            .addOnPlatformViewCreatedListener(_createCardCollectController);
        platformView.create();
        return platformView;
      },
    );
  }

  Widget _collectButton() {
    return MaterialButton(
      shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8.0)),
      color: Color(0xff3c4c5d),
      child: new Text('Submit',
          style: new TextStyle(fontSize: 16.0, color: Colors.white)),
      onPressed: () {
        collectController.redactCardAsync().then((value) {
          final entries = value.entries;
          print("value.entries: $entries");

          final errorCode = value[COLLECT_ERROR_CODE_KEY];
          if (errorCode != null) {
            print("error!");
            print("VGS Collect SDK error code: $errorCode");

            final errorMessage = value[COLLECT_ERROR_MESSAGE_KEY];
            if (errorMessage != null) {
              print("VGS Collect SDK error message: $errorMessage");
            }
            return;
          }

          cardToken = value.entries
              .firstWhere((element) => element.key == CARD_TOKEN_KEY)
              .value;
          dateToken = value.entries
              .firstWhere((element) => element.key == DATE_TOKEN_KEY)
              .value;

          print("cardToken from collect: $cardToken");
          print("dateToken from collect: $dateToken");
        });
      },
    );
  }

  void _createCardCollectController(int id) {
    print("View id = $id");
    collectController = new CardCollectFormController._(id);
  }

  Widget _cardShow() {
    final Map<String, dynamic> creationParams = <String, dynamic>{};
    return PlatformViewLink(
      viewType: SHOW_FORM_VIEW_TYPE,
      surfaceFactory:
          (BuildContext context, PlatformViewController controller) {
        return AndroidViewSurface(
          controller: controller,
          gestureRecognizers: const <Factory<OneSequenceGestureRecognizer>>{},
          hitTestBehavior: PlatformViewHitTestBehavior.opaque,
        );
      },
      onCreatePlatformView: (PlatformViewCreationParams params) {
        var platformView = PlatformViewsService.initSurfaceAndroidView(
          id: params.id,
          viewType: SHOW_FORM_VIEW_TYPE,
          layoutDirection: TextDirection.ltr,
          creationParams: creationParams,
          creationParamsCodec: StandardMessageCodec(),
        );
        platformView
            .addOnPlatformViewCreatedListener(params.onPlatformViewCreated);
        platformView
            .addOnPlatformViewCreatedListener(_createCardShowController);
        platformView.create();
        return platformView;
      },
    );
  }

  Widget _cardCollectNativeiOS() {
    final Map<String, dynamic> creationParams = <String, dynamic>{};
    return Column(children: [
      SizedBox(
          height: 136.0,
          child: UiKitView(
              viewType: COLLECT_FORM_VIEW_TYPE,
              onPlatformViewCreated: _createCardCollectController,
              creationParams: creationParams,
              creationParamsCodec: StandardMessageCodec()))
    ]);
  }

  Widget _cardShowNativeiOS() {
    final Map<String, dynamic> creationParams = <String, dynamic>{};
    return Column(children: [
      SizedBox(
          height: 136.0,
          child: UiKitView(
              viewType: SHOW_FORM_VIEW_TYPE,
              onPlatformViewCreated: _createCardShowController,
              creationParams: creationParams,
              creationParamsCodec: StandardMessageCodec()))
    ]);
  }

  Widget _showButton() {
    return MaterialButton(
      shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8.0)),
      color: Color(0xff3c4c5d),
      child: new Text('Reveal',
          style: new TextStyle(fontSize: 16.0, color: Colors.white)),
      onPressed: () {
        showController.revealCardAsync(cardToken, dateToken).then((value) {
          final entries = value.entries;
          print("show value.entries: $entries");

          final errorCode = value[SHOW_ERROR_CODE_KEY];
          if (errorCode != null) {
            print("error!");
            print("VGS Show SDK error code: $errorCode");

            final errorMessage = value[SHOW_ERROR_MESSAGE_KEY];
            if (errorMessage != null) {
              print("VGS Show SDK error message: $errorMessage");
            }
            return;
          }

          final successStatusCode = value[SHOW_SUCCESS_CODE_KEY];
          if (successStatusCode != null) {
            print("VGS Show success status code: $successStatusCode");
          }
        });
      },
    );
  }

  void _createCardShowController(int id) {
    print("Show View id = $id");
    showController = new CardShowFormController._(id);
  }
}

class CardCollectFormController {
  CardCollectFormController._(int id)
      : _channel = new MethodChannel('$COLLECT_FORM_VIEW_TYPE/$id');

  final MethodChannel _channel;

  Future<Map<dynamic, dynamic>> redactCardAsync() async {
    return await _channel.invokeMethod('redactCard', null);
  }
}

class CardShowFormController {
  CardShowFormController._(int id)
      : _channel = new MethodChannel('$SHOW_FORM_VIEW_TYPE/$id');

  final MethodChannel _channel;

  Future<Map<dynamic, dynamic>> revealCardAsync(
      String cardToken, String dateToken) async {
    return await _channel.invokeMethod('revealCard', [cardToken, dateToken]);
  }
}
dmytrokhl commented 2 years ago

Thanks @akimau , we will check the issue.

akimau commented 2 years ago

Thanks @akimau , we will check the issue.

Cheers @dmytrokhl

DmytroDm commented 2 years ago

@akimau Hi, thanks for informing us about the possible bug.

I have tried your code example, unfortunately, I can’t reproduce the bug. Can you please share a screen recording to see the exact steps on how to reproduce it?

Also, can you provide more details about the emulator you using to debug?

akimau commented 2 years ago

Thank you @dmytrokhl, @DmytroDm for your response. I have done a screen recording below:

https://user-images.githubusercontent.com/57058653/137477409-2e02ea16-d0ee-4744-8e58-ea5ff505f09a.mp4

From the video, when I launch the app and go straight to vgs collect screen without inputting text in the dummy example screen, I am able to input card number. Once I go back and type into any of the textfields in the dummy example screen, and navigate to the vgs collect screen again, I am unable to input card number this time around. Seems like when it refuses to input card number and then you press next on the keyboard, so that the card number field looses focus, then you try re-type into the field again this time it accepts. I have experienced this in a real device and also emulator... The video shot is done when running the app on Android emulator with ADB serial no. emulator-5554, and android version 11.0 (R) - API 30. Emulator version is 30.8.4-7600983. On a real device I have experienced it on Samsung A50, that has Android 11.

My flutter doctor log is as follows

➜ ~ flutter doctor -v [✓] Flutter (Channel stable, 2.5.2, on macOS 11.5.2 20G95 darwin-x64 ) • Flutter version 2.5.2 at ~/Development/flutter • Upstream repository https://github.com/flutter/flutter.git • Framework revision 3595343e20 (2 weeks ago), 2021-09-30 12:58:18 -0700 • Engine revision 6ac856380f • Dart version 2.14.3

[✓] Android toolchain - develop for Android devices (Android SDK version 30.0.3) • Android SDK at ~/Library/Android/sdk • Platform android-31, build-tools 30.0.3 • ANDROID_HOME = ~/Library/Android/sdk • ANDROID_SDK_ROOT = ~/Library/Android/sdk • Java binary at: /Applications/Android Studio.app/Contents/jre/Contents/Home/bin/java • Java version OpenJDK Runtime Environment (build 11.0.10+0-b96-7281165) • All Android licenses accepted.

[✓] Xcode - develop for iOS and macOS • Xcode at /Applications/Xcode.app/Contents/Developer • Xcode 13.0, Build version 13A233 • CocoaPods version 1.10.1

[✓] Android Studio (version 2020.3) • Android Studio at /Applications/Android Studio.app/Contents • Flutter plugin can be installed from: 🔨 https://plugins.jetbrains.com/plugin/9212-flutter • Dart plugin can be installed from: 🔨 https://plugins.jetbrains.com/plugin/6351-dart • Java version OpenJDK Runtime Environment (build 11.0.10+0-b96-7281165)

[✓] IntelliJ IDEA Community Edition (version 2020.3.4) • IntelliJ at /Applications/IntelliJ IDEA CE.app • Flutter plugin version 59.0.2 • Dart plugin version 203.8292

[✓] VS Code (version 1.61.1) • VS Code at /Applications/Visual Studio Code.app/Contents • Flutter extension version 3.27.0

[✓] Connected device (1 available) • sdk gphone x86 (mobile) • emulator-5554 • android-x86 • Android 11 (API 30) (emulator)

• No issues found! ➜ ~

akimau commented 2 years ago

Thank you @dmytrokhl, @DmytroDm for your response. I have done a screen recording below:

vgs.bug.demo.mp4 From the video, when I launch the app and go straight to vgs collect screen without inputting text in the dummy example screen, I am able to input card number. Once I go back and type into any of the textfields in the dummy example screen, and navigate to the vgs collect screen again, I am unable to input card number this time around. Seems like when it refuses to input card number and then you press next on the keyboard, so that the card number field looses focus, then you try re-type into the field again this time it accepts. I have experienced this in a real device and also emulator... The video shot is done when running the app on Android emulator with ADB serial no. emulator-5554, and android version 11.0 (R) - API 30. Emulator version is 30.8.4-7600983. On a real device I have experienced it on Samsung A50, that has Android 11.

My flutter doctor log is as follows

➜ ~ flutter doctor -v [✓] Flutter (Channel stable, 2.5.2, on macOS 11.5.2 20G95 darwin-x64 ) • Flutter version 2.5.2 at ~/Development/flutter • Upstream repository https://github.com/flutter/flutter.git • Framework revision 3595343e20 (2 weeks ago), 2021-09-30 12:58:18 -0700 • Engine revision 6ac856380f • Dart version 2.14.3

[✓] Android toolchain - develop for Android devices (Android SDK version 30.0.3) • Android SDK at ~/Library/Android/sdk • Platform android-31, build-tools 30.0.3 • ANDROID_HOME = ~/Library/Android/sdk • ANDROID_SDK_ROOT = ~/Library/Android/sdk • Java binary at: /Applications/Android Studio.app/Contents/jre/Contents/Home/bin/java • Java version OpenJDK Runtime Environment (build 11.0.10+0-b96-7281165) • All Android licenses accepted.

[✓] Xcode - develop for iOS and macOS • Xcode at /Applications/Xcode.app/Contents/Developer • Xcode 13.0, Build version 13A233 • CocoaPods version 1.10.1

[✓] Android Studio (version 2020.3) • Android Studio at /Applications/Android Studio.app/Contents • Flutter plugin can be installed from: 🔨 https://plugins.jetbrains.com/plugin/9212-flutter • Dart plugin can be installed from: 🔨 https://plugins.jetbrains.com/plugin/6351-dart • Java version OpenJDK Runtime Environment (build 11.0.10+0-b96-7281165)

[✓] IntelliJ IDEA Community Edition (version 2020.3.4) • IntelliJ at /Applications/IntelliJ IDEA CE.app • Flutter plugin version 59.0.2 • Dart plugin version 203.8292

[✓] VS Code (version 1.61.1) • VS Code at /Applications/Visual Studio Code.app/Contents • Flutter extension version 3.27.0

[✓] Connected device (1 available) • sdk gphone x86 (mobile) • emulator-5554 • android-x86 • Android 11 (API 30) (emulator)

• No issues found! ➜ ~

Any feedback on this @DmytroDm Could it be the latest flutter/dart version causing this?

akimau commented 2 years ago

@dmytrokhl, When I changed inputType for cvv to numberPassword, the issue disappeared for CardVerificationCodeEditText. Still there though for VGSCardNumberEditText. I have to go back previous screen and launch card collect screen again, for it to allow keyboard input.

DmytroDm commented 2 years ago

@akimau Hi, thank you for providing info about your setup, it's very helpful. After investigation, I noticed a couple of things:

So, I can suggest downgrading your flutter version to 1.22.6. Also, we will notify you if we will have any updates about this.

akimau commented 2 years ago

@akimau Hi, thank you for providing info about your setup, it's very helpful. After investigation, I noticed a couple of things:

  • This bug can be reproduced on flutter version 2.0.0 and newer, I've tested these steps on the flutter version 1.22.6 and everything works like expected.
  • I've replaced VGSCardNumberEditText with native android EditText with inputType="number" and the bug still exists on flutter version 2.0.0 and newer, so I assume that this bug is related to flutter.

So, I can suggest downgrading your flutter version to 1.22.6. Also, we will notify you if we will have any updates about this.

Thank you @DmytroDm for helping in investigating the issue. We look forward then for the bug being resolved by the Flutter team for Flutter version 2 since it seems it's a keyboard issue with Flutter android native view embedding. I will also gladly await any updates you have on this issue even as I close it.

akimau commented 2 years ago

Actually, I have discovered the solution to the above issue is specifying the onFocus callback in initSurfaceAndroidView as follows:

PlatformViewsService.initSurfaceAndroidView(
        ...
        onFocus: () => params.onFocusChanged(true),
 )

With this, we don't need to downgrade flutter version. Works with Flutter version 2.0

Cheers!