diff --git a/android/build.gradle b/android/build.gradle index 0ee5253..46fb6e7 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -43,6 +43,6 @@ android { } dependencies { - implementation 'com.plaid.link:sdk-core:4.5.1' + implementation 'com.plaid.link:sdk-core:4.6.0' } } diff --git a/android/src/main/java/com/github/jorgefspereira/plaid_flutter/PlaidFlutterPlugin.java b/android/src/main/java/com/github/jorgefspereira/plaid_flutter/PlaidFlutterPlugin.java index 5286e2c..96fcba7 100644 --- a/android/src/main/java/com/github/jorgefspereira/plaid_flutter/PlaidFlutterPlugin.java +++ b/android/src/main/java/com/github/jorgefspereira/plaid_flutter/PlaidFlutterPlugin.java @@ -25,6 +25,8 @@ import kotlin.Unit; import com.plaid.link.Plaid; +import com.plaid.link.PlaidHandler; +import com.plaid.link.SubmissionData; import com.plaid.link.configuration.LinkTokenConfiguration; import com.plaid.link.event.LinkEventMetadata; import com.plaid.link.result.LinkAccount; @@ -43,6 +45,9 @@ public class PlaidFlutterPlugin implements FlutterPlugin, MethodCallHandler, Eve private static final String TOKEN = "token"; private static final String NO_LOADING_STATE = "noLoadingState"; + /// SubmissionData + private static final String PHONE_NUMBER = "phoneNumber"; + /// LinkResultHandler private static final String EVENT_ON_SUCCESS = "success"; private static final String EVENT_ON_EXIT = "exit"; @@ -58,6 +63,7 @@ public class PlaidFlutterPlugin implements FlutterPlugin, MethodCallHandler, Eve private MethodChannel methodChannel; private EventChannel eventChannel; private EventSink eventSink; + private PlaidHandler plaidHandler; /// Result handler private final LinkResultHandler resultHandler = new LinkResultHandler( @@ -112,15 +118,23 @@ public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) { @Override public void onMethodCall(MethodCall call, @NonNull Result result) { - if(call.method.equals("open")) { - this.open(call.arguments(), result); - } - else if(call.method.equals("close")) { - this.close(result); - } - else { - result.notImplemented(); - } + switch (call.method) { + case "create": + this.create(call.arguments(), result); + break; + case "open": + this.open(result); + break; + case "close": + this.close(result); + break; + case "submit": + this.submit(call.arguments(), result); + break; + default: + result.notImplemented(); + break; + } } /// ActivityAware @@ -174,7 +188,7 @@ private void sendEvent(Object argument) { /// Exposed methods - private void open(Map arguments, Result reply) { + private void create(Map arguments, Result reply) { if (binding == null) { reply.error("PlaidFlutter", "Activity not attached", null); return; @@ -193,11 +207,18 @@ private void open(Map arguments, Result reply) { LinkTokenConfiguration config = getLinkTokenConfiguration(arguments); if(config != null) { - Plaid.create((Application)context.getApplicationContext(),config).open(binding.getActivity()); - reply.success(null); - } else { - reply.error("-1", "Create was not called.", "Unable to create LinkTokenConfiguration"); + plaidHandler = Plaid.create((Application)context.getApplicationContext(),config); } + + reply.success(null); + } + + private void open(Result reply) { + if (plaidHandler != null) { + plaidHandler.open(binding.getActivity()); + } + + reply.success(null); } private void close(Result reply) { @@ -210,6 +231,22 @@ private void close(Result reply) { reply.success(null); } + private void submit(Map arguments, Result reply) { + if (arguments == null) { + reply.success(null); + return; + } + + String phoneNumber = (String) arguments.get(PHONE_NUMBER); + + if (plaidHandler != null && phoneNumber != null) { + SubmissionData submissionData = new SubmissionData(phoneNumber); + plaidHandler.submit(submissionData); + } + + reply.success(null); + } + /// Configuration Parsing private LinkTokenConfiguration getLinkTokenConfiguration(Map arguments) { diff --git a/example/lib/main.dart b/example/lib/main.dart index bfd4c24..6433ca0 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -3,9 +3,11 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:plaid_flutter/plaid_flutter.dart'; -void main() => runApp(MyApp()); +void main() => runApp(const MyApp()); class MyApp extends StatefulWidget { + const MyApp({super.key}); + @override _MyAppState createState() => _MyAppState(); } @@ -36,8 +38,8 @@ class _MyAppState extends State { void _createLinkTokenConfiguration() { setState(() { - _configuration = LinkTokenConfiguration( - token: "GENERATED_LINK_TOKEN", + _configuration = const LinkTokenConfiguration( + token: "link-sandbox-74cf082e-870b-461f-a37a-038cace0afee", ); }); } @@ -81,14 +83,29 @@ class _MyAppState extends State { ), ElevatedButton( onPressed: _createLinkTokenConfiguration, - child: Text("Create Link Token Configuration"), + child: const Text("Create Link Token Configuration"), + ), + const SizedBox(height: 15), + ElevatedButton( + onPressed: _configuration != null + ? () { + PlaidLink.create(configuration: _configuration!); + PlaidLink.open(); + } + : null, + child: const Text("Open"), ), - SizedBox(height: 15), ElevatedButton( onPressed: _configuration != null - ? () => PlaidLink.open(configuration: _configuration!) + ? () { + PlaidLink.submit( + SubmissionData( + phoneNumber: "14155550015", + ), + ); + } : null, - child: Text("Open"), + child: const Text("Submit Phone Number"), ), Expanded( child: Center( diff --git a/ios/Classes/PlaidFlutterPlugin.m b/ios/Classes/PlaidFlutterPlugin.m index 5b40b51..d75bf01 100644 --- a/ios/Classes/PlaidFlutterPlugin.m +++ b/ios/Classes/PlaidFlutterPlugin.m @@ -2,6 +2,7 @@ #import static NSString* const kTokenKey = @"token"; +static NSString* const kPhoneNumberKey = @"phoneNumber"; static NSString* const kContinueRedirectUriKey = @"redirectUri"; static NSString* const kNoLoadingStateKey = @"noLoadingState"; static NSString* const kOnSuccessType = @"success"; @@ -19,6 +20,7 @@ @interface PlaidFlutterPlugin () @implementation PlaidFlutterPlugin { FlutterEventSink _eventSink; id _linkHandler; + NSError *creationError; } + (void)registerWithRegistrar:(NSObject *)registrar { @@ -38,12 +40,16 @@ - (void)dealloc { } - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { - if ([@"open" isEqualToString:call.method]) - [self openWithArguments: call.arguments withResult:result]; + if ([@"create" isEqualToString:call.method]) + [self createWithArguments: call.arguments withResult:result]; + else if ([@"open" isEqualToString:call.method]) + [self openWithResult:result]; else if ([@"close" isEqualToString:call.method]) [self closeWithResult:result]; else if([@"resumeAfterTermination" isEqualToString:call.method]) [self resumeAfterTermination:call.arguments withResult:result]; + else if([@"submit" isEqualToString:call.method]) + [self submit:call.arguments withResult:result]; else result(FlutterMethodNotImplemented); @@ -71,7 +77,7 @@ - (FlutterError *)onCancelWithArguments:(id)arguments { #pragma mark Exposed methods -- (void) openWithArguments: (id _Nullable)arguments withResult:(FlutterResult)result { +- (void) createWithArguments: (id _Nullable)arguments withResult:(FlutterResult)result { NSString* token = arguments[kTokenKey]; BOOL noLoadingState = arguments[kNoLoadingStateKey]; @@ -119,12 +125,17 @@ - (void) openWithArguments: (id _Nullable)arguments withResult:(FlutterResult)re config.onExit = exitHandler; config.noLoadingState = noLoadingState; - NSError *creationError = nil; - _linkHandler = [PLKPlaid createWithLinkTokenConfiguration:config error:&creationError]; + NSError *error = nil; + _linkHandler = [PLKPlaid createWithLinkTokenConfiguration:config error:&error]; + creationError = error; + + result(nil); +} +- (void) openWithResult:(FlutterResult)result { if (_linkHandler) { __block bool didPresent = NO; - + __weak typeof(self) weakSelf = self; /// void(^presentationHandler)(UIViewController *) = ^(UIViewController *linkViewController) { UIViewController* rootViewController = [UIApplication sharedApplication].delegate.window.rootViewController; @@ -139,12 +150,11 @@ - (void) openWithArguments: (id _Nullable)arguments withResult:(FlutterResult)re } }; - [_linkHandler openWithPresentationHandler:presentationHandler dismissalHandler:dismissalHandler]; result(nil); } else { - + NSString *errorMessage = creationError ? creationError.userInfo[@"message"] : @"Create was not called."; NSString *errorCode = creationError ? [@(creationError.code) stringValue] : @"-1"; NSString *errorDetails = @"Unable to create PLKHandler"; @@ -185,7 +195,7 @@ - (void) closeWithResult:(FlutterResult)result { result(nil); } -- (void) resumeAfterTermination: (id _Nullable)arguments withResult:(FlutterResult)result{ +- (void) resumeAfterTermination: (id _Nullable)arguments withResult:(FlutterResult)result{ NSString* redirectUriString = arguments[kContinueRedirectUriKey]; NSURL *redirectUriURL = (id)redirectUriString == [NSNull null] ? nil : [NSURL URLWithString:redirectUriString]; @@ -196,6 +206,18 @@ - (void) resumeAfterTermination: (id _Nullable)arguments withResult:(FlutterRes result(nil); } +- (void) submit: (id _Nullable)arguments withResult:(FlutterResult)result{ + NSString* phoneNumber = arguments[kPhoneNumberKey]; + + if (_linkHandler && phoneNumber) { + PLKSubmissionData *data = [[PLKSubmissionData alloc] init]; + data.phoneNumber = phoneNumber; + [_linkHandler submit: data]; + } + + result(nil); +} + #pragma mark PLKConfiguration - (PLKLinkTokenConfiguration*)getLinkTokenConfigurationWithToken: (NSString *)token onSuccessHandler:(PLKOnSuccessHandler)successHandler{ diff --git a/lib/src/core/link_configuration.dart b/lib/src/core/link_configuration.dart index dde99f5..55898fb 100644 --- a/lib/src/core/link_configuration.dart +++ b/lib/src/core/link_configuration.dart @@ -1,7 +1,3 @@ -/// The LinkTokenConfiguration only needs a link_token which is created by your app's -/// server and passed to your app's client to initialize Link. The Link configuration parameters that were -/// previously set within Link itself are now set via parameters passed to /link/token/create and conveyed -/// to Link via the link_token. class LinkTokenConfiguration { /// Specify a link_token to authenticate your app with Link. This is a short lived, one-time use token that should be unique for each Link session final String token; @@ -12,6 +8,13 @@ class LinkTokenConfiguration { /// WEB ONLY: A receivedRedirectUri is required to support OAuth authentication flows when re-launching Link on a mobile device. final String? receivedRedirectUri; + /// The LinkTokenConfiguration only needs a token which is created by your app's + /// server and passed to your app's client to initialize Link. The Link configuration parameters that were + /// previously set within Link itself are now set via parameters passed to /link/token/create and conveyed + /// to Link via the link_token. + /// + /// Note that each time you open Link, you will need to get a new link_token from your server and create a new LinkTokenConfiguration with it. + /// const LinkTokenConfiguration({ required this.token, this.noLoadingState = false, @@ -38,3 +41,19 @@ class LinkTokenConfiguration { int get hashCode => Object.hash( token.hashCode, noLoadingState.hashCode, receivedRedirectUri.hashCode); } + +/// Data to submit during a Link session. +class SubmissionData { + /// The end user's phone number. + final String phoneNumber; + + SubmissionData({ + required this.phoneNumber, + }); + + Map toJson() { + return { + 'phoneNumber': phoneNumber, + }; + } +} diff --git a/lib/src/core/plaid_link.dart b/lib/src/core/plaid_link.dart index 9da9e64..aa921e7 100644 --- a/lib/src/core/plaid_link.dart +++ b/lib/src/core/plaid_link.dart @@ -31,10 +31,15 @@ class PlaidLink { static Stream get onExit => _platform.onObject.where((event) => event is LinkExit).cast(); - /// Initializes the Plaid Link flow on the device. - static Future open( + /// Creates a handler for Plaid Link. A one-time use object used to open a Link session. + static Future create( {required LinkTokenConfiguration configuration}) async { - await _platform.open(configuration: configuration); + await _platform.create(configuration: configuration); + } + + /// Open Plaid Link by calling open on the Handler object. + static Future open() async { + await _platform.open(); } /// Closes Plaid Link View @@ -46,4 +51,9 @@ class PlaidLink { static Future resumeAfterTermination(String redirectUri) async { await _platform.resumeAfterTermination(redirectUri); } + + /// It allows the client application to submit additional user-collected data to the Link flow (e.g. a user phone number) for the Layer product. + static Future submit(SubmissionData data) async { + await _platform.submit(data); + } } diff --git a/lib/src/platform/plaid_flutter_web.dart b/lib/src/platform/plaid_flutter_web.dart index 57e4978..a71b358 100644 --- a/lib/src/platform/plaid_flutter_web.dart +++ b/lib/src/platform/plaid_flutter_web.dart @@ -31,9 +31,9 @@ class PlaidFlutterPlugin extends PlaidPlatformInterface { PlaidPlatformInterface.instance = PlaidFlutterPlugin(); } - /// Initializes the Plaid Link flow on the device. + /// Creates a handler for Plaid Link. A one-time use object used to open a Link session. @override - Future open({required LinkTokenConfiguration configuration}) async { + Future create({required LinkTokenConfiguration configuration}) async { WebConfiguration options = WebConfiguration(); /// onSuccess handler @@ -77,6 +77,11 @@ class PlaidFlutterPlugin extends PlaidPlatformInterface { options.env = configuration.token.split('-')[1]; _plaid = await Plaid.create(options); + } + + /// Open Plaid Link by calling open on the Handler object. + @override + Future open() async { _plaid?.open(); } diff --git a/lib/src/platform/plaid_method_channel.dart b/lib/src/platform/plaid_method_channel.dart index beb9810..dff48be 100644 --- a/lib/src/platform/plaid_method_channel.dart +++ b/lib/src/platform/plaid_method_channel.dart @@ -32,10 +32,16 @@ class PlaidMethodChannel extends PlaidPlatformInterface { return _onObject!; } - /// Initializes the Plaid Link flow on the device. + /// Creates a handler for Plaid Link. A one-time use object used to open a Link session. @override - Future open({required LinkTokenConfiguration configuration}) async { - await _channel.invokeMethod('open', configuration.toJson()); + Future create({required LinkTokenConfiguration configuration}) async { + await _channel.invokeMethod('create', configuration.toJson()); + } + + /// Open Plaid Link by calling open on the Handler object. + @override + Future open() async { + await _channel.invokeMethod('open'); } /// Closes Plaid Link View @@ -52,4 +58,10 @@ class PlaidMethodChannel extends PlaidPlatformInterface { {"redirectUri": redirectUri}, ); } + + /// It allows the client application to submit additional user-collected data to the Link flow (e.g. a user phone number) for the Layer product. + @override + Future submit(SubmissionData data) async { + await _channel.invokeMethod('submit', data.toJson()); + } } diff --git a/lib/src/platform/plaid_platform_interface.dart b/lib/src/platform/plaid_platform_interface.dart index 72bd6aa..bc532b3 100644 --- a/lib/src/platform/plaid_platform_interface.dart +++ b/lib/src/platform/plaid_platform_interface.dart @@ -1,6 +1,7 @@ -import 'package:plaid_flutter/plaid_flutter.dart'; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; +import '../core/events.dart'; +import '../core/link_configuration.dart'; import 'plaid_method_channel.dart'; abstract class PlaidPlatformInterface extends PlatformInterface { @@ -26,8 +27,13 @@ abstract class PlaidPlatformInterface extends PlatformInterface { throw UnimplementedError('onObject has not been implemented.'); } - /// Initializes the Plaid Link flow on the device. - Future open({required LinkTokenConfiguration configuration}) async { + /// Creates a handler for Plaid Link. A one-time use object used to open a Link session. + Future create({required LinkTokenConfiguration configuration}) async { + throw UnimplementedError('create() has not been implemented.'); + } + + /// Open Plaid Link by calling open on the Handler object. + Future open() async { throw UnimplementedError('open() has not been implemented.'); } @@ -41,4 +47,9 @@ abstract class PlaidPlatformInterface extends PlatformInterface { throw UnimplementedError( 'resumeAfterTermination() has not been implemented.'); } + + /// It allows the client application to submit additional user-collected data to the Link flow (e.g. a user phone number) for the Layer product. + Future submit(SubmissionData data) async { + throw UnimplementedError('submit() has not been implemented.'); + } } diff --git a/test/core/plaid_link_test.dart b/test/core/plaid_link_test.dart index 5590533..181bc3e 100644 --- a/test/core/plaid_link_test.dart +++ b/test/core/plaid_link_test.dart @@ -95,15 +95,15 @@ main() { const linkTokenConfiguration = LinkTokenConfiguration(token: token); when( - () => plaidPlatformInterface.open( + () => plaidPlatformInterface.create( configuration: linkTokenConfiguration, ), ).thenAnswer(Future.value); - await PlaidLink.open(configuration: linkTokenConfiguration); + await PlaidLink.open(); verify( - () => plaidPlatformInterface.open( + () => plaidPlatformInterface.create( configuration: linkTokenConfiguration, ), ).called(1);