simonlevy5 / plaid-ios-link

Plaid Link experience built natively for iOS
MIT License
34 stars 10 forks source link

Account ID #14

Open sinanku opened 8 years ago

sinanku commented 8 years ago

Is there a way to retrieve the account ID through the callback for access_token? (also for future clarification I believe this is the public_token not access_token to be sent to Plaid).

Thanks for the awesome library @simonlevy5 !

plaidClient.exchangeToken(public_token, need this -> PLAID_ACCOUNT_ID, function(err, tokenResponse) {
      // respond with access_token here
}

Edit: I was thinking something along the lines of the selectAccount feature on the web Plaid link where

func linkNavigationContoller(navigationController: PLDLinkNavigationViewController!, didFinishWithAccessToken accessToken: String!) {
     // do stuff
}
func linkNavigationContoller(navigationController: PLDLinkNavigationViewController!, didFinishWithAccessToken accessToken: String!, didRetrieveAccountId accountId: String) {
     // do more stuff based on account selection
}
sinanku commented 8 years ago

Posting a bounty for a similar authentication flow to Plaid Link multi-accounts.

https://www.bountysource.com/issues/35126919-account-id

simonlevy5 commented 8 years ago

@sinanulkuatam There isn't one now, but I can probably add one. This is new to me so I'll need to investigate a bit first. If you are interested in helping let me know, too, so I can provide some guidance.

I would likely add a selectAccount option to the options NSDictionary on PLDLinkNavigationController rather than as a parameter to exchangeToken.

@michaelckelly you guys have a demo of this feature that I can use?

sinanku commented 8 years ago

@simonlevy5 excellent! I am more familiar with Swift than Objective-C but I'd be more than happy to investigate as well tonight. I've opened up a Slack group https://openhq.slack.com/home as well as https://tlk.io/argos

sinanku commented 8 years ago

@simonlevy5 So in the Plaid.h add and step methods the options NSDictionary passed in would include the accountId as an optional parameter (just brainstorming here)

- (void)stepLinkUserForProduct:(PlaidProduct)product
                   publicToken:(NSString *)publicToken
                   ??accountId:(NSString *)accountId??
                   mfaResponse:(id)mfaResponse
                       options:(NSDictionary *)options
                    completion:(PlaidMfaCompletion)completion;

And then in the implementation how would it account for the accountId being passed in as an options variable? @michaelckelly it would be good to get your thoughts on whether it would make more sense to put in an explicit account accountId:(NSString )accountId or to think of options:(NSDictionary )options as being the metadata response (like metadata:(NSDictionary *)metadata) returned on the Plaid Link web module.

- (void)stepLinkUserForProduct:(PlaidProduct)product
                   publicToken:(NSString *)publicToken
                   ??accountId:(NSString *)accountId??
                   mfaResponse:(id)mfaResponse
                       options:(NSDictionary *)options
                    completion:(PlaidMfaCompletion)completion {

  NSString *webhook = @"";
  if (options[@"webhook"]) {
    webhook = options[@"webhook"];
  }
  NSDictionary *parameters = @{
    @"env" : NSStringFromPlaidEnviroment(_environment),
    @"include_accounts": @(NO),
    @"mfa": mfaResponse,
    @"product": NSStringFromPlaidProduct(product),
    @"public_key": _publicKey,
    @"public_token": publicToken,
    @"account_id": accountId, --> this ends up returning nil
    @"webhook": webhook
  };
  [_networkApi executeRequestWithHost:LinkHostForEnvironment(_environment)
                                 path:@"authenticate/mfa"
                               method:@"POST"
                           parameters:parameters
                           completion:^(id response, NSError *error) {
    if (error) {
     completion(nil, response, error);
     return;
    }

    PLDLinkAuthentication *authentication =
        [[PLDLinkAuthentication alloc] initWithProduct:product
                                              response:response];
    completion(authentication, response, error);
  }];
}

And in the callback where the accountId would be available and we could pass it in to any function to send off to the backend.

/**
 Called when the PLDLinkNavigationController has successfully logged a user in and obtained an access token for that user.

 @param navigationController The navigation controller presenting Plaid Link.
 @param accessToken A valid access token for a user to access the Plaid system.
 */
- (void)linkNavigationContoller:(PLDLinkNavigationViewController *)navigationController
       didFinishWithAccessToken:(NSString *)accessToken
       didFinishWithAccountId:(NSString *)accountId;

And in the Swift implementation

func linkNavigationContoller(navigationController: PLDLinkNavigationViewController!, didFinishWithAccessToken accessToken: String!, didFinishWithAccountId accountId: String!) {
        print("success \(accessToken)")
        linkPlaidBankAccount({ (stripeBankToken, accessToken) in
                // completion handler with the necessary tokens
        }, accessToken: accessToken, accountId: accountId) // send these two to the backend
}

func linkPlaidBankAccount(completionHandler: (String, String) -> Void, accessToken: String, accountId: String) {
     /* perform api request with the accessToken (in reality the publicToken) and the accountId
        to request and exchange with an accessToken
     */
}

Let me know if my line of thinking/reasoning is off because I'm still pretty new to this.

simonlevy5 commented 8 years ago

@sinanulkuatam Since the accountId is something that isn't required it is an optional parameter. As you can notice we have a good example of this, which is "webhook". What that means is there is no interface change required within the Plaid.h since the parameter would be added right to the options NSDictionary.

I'm happy to take on this change, but would like to know more about it first - mainly what does this experience even look like (why I asked the question above), as well as what the API behavior is. Please understand that this is an undocumented call - normally the account data is fetched after the access token is retrieved via the exchange endpoint (using the token returned by link) so i'm unclear as to the requests/responses expected, as well.

sinanku commented 8 years ago

@simonlevy5 understood. Going through the web documentation it looks like the request/response cycle looks like this below, where the account id is returned in the onSuccess function of calling Plaid Auth (not connect). If I were to use this in production as the library currently stands what flow would you envision the in its usage (use connect only)? I feel that creating this may take some time, and time is a luxury at the moment :) so I greatly appreciate you taking a look into this.

Attached some images of the user flow below as well from the web interface.

var linkHandler = Plaid.create({
  selectAccount: true,
  env: 'production',
  clientName: 'Plaid Name',
  key: 'plaid-key-here',
  product: 'auth',
  onSuccess: function(public_token, metadata) {
    // The onSuccess function is called when the user has successfully
    // authenticated and selected an account to use.
    //
    // When called, you will send the public_token and the selected
    // account ID, metadata.account_id, to your backend app server.
    //
    // sendDataToBackendServer({
    //   public_token: public_token,
    //   account_id: metadata.account_id
    // });
    document.getElementById('plaidToken').value = public_token;
    document.getElementById('plaidAcctId').value = metadata.account_id;
    document.getElementById('plaidForm').submit();
  },
});

So I assume that after the authentication occurs with the simple curl

curl -X POST https://tartan.plaid.com/connect \
  -d client_id=test_id \
  -d secret=test_secret \
  -d username=plaid_test \
  -d password=plaid_good \
  -d type=wells

It will respond with a series of accounts

http code 200
{
  "accounts": [
    {object},
    {object}
  ],
  "transactions": [
    {object},
    {object},
    {object}
  ],
  "access_token": "xxxxx"
}

And these accounts (and their respective IDs) I believe are necessary for exchanging a Stripe token if I am not mistaken...

From the Plaid node library documentation

Exchange a public_token and account_id from the Plaid + Stripe ACH integration for a Plaid access token and a Stripe bank account token:

plaidClient.exchangeToken(public_token, account_id, function(err, res) {
  var access_token = res.access_token;
  var stripe_token = res.stripe_bank_account_token;

  // Use the access_token to make make Plaid API requests.
  // Use the Stripe token to make Stripe ACH API requests.
});

While the normal Auth type authentication is more straightforward, just give a public_token and get and access_token back which is what currently exists

Exchange a public_token from Plaid Link for a Plaid access token and then retrieve account data:

plaidClient.exchangeToken(public_token, function(err, res) {
  var access_token = res.access_token;

  plaidClient.getAuthUser(access_token, function(err, res) {
    console.log(res.accounts);
  });
});

From Plaids API docs in both Connect and Auth the accounts as mentioned above are returned in an array, and this shouldn't change from the current flow as it works well.

  "accounts": [
    {
      "_id": "mjj9jp92z2fD1mLlpQYZI1gAd4q4LwTKmBNLz",

From here creating a table while tying the id of the account to the table rows should suffice in a didSelectRowAtIndexPath call and ultimately determine what gets sent to the api. The same access token can apply to all, but the account id will differ.

Here's the web flow for a UI reference.

plaid_0 plaid_3 plaid_1

A couple of quick mockups I whipped up

link1 link2 link3 link4 link5

A small currency formatting utility I wrote for the currency formatting (in Swift)

func formatCurrency(amount: String, fontName: String, superSize: CGFloat, fontSize: CGFloat, offsetSymbol: Int, offsetCents: Int) -> NSAttributedString {
    let formatter = NSNumberFormatter()
    formatter.numberStyle = .CurrencyStyle
    let r = amount.startIndex..<amount.endIndex
    let x = amount.substringWithRange(r)
    let amt = formatter.stringFromNumber(Float(x)!/100)
    let font:UIFont? = UIFont(name: fontName, size: fontSize)
    let fontSuper:UIFont? = UIFont(name: fontName, size: superSize)
    let attString:NSMutableAttributedString = NSMutableAttributedString(string: amt!, attributes: [
        NSFontAttributeName:font!
    ])
    if Float(x) < 0 {
        attString.setAttributes([NSFontAttributeName:fontSuper!,NSBaselineOffsetAttributeName:offsetSymbol], range: NSRange(location:1,length:1))
        attString.setAttributes([NSFontAttributeName:fontSuper!,NSBaselineOffsetAttributeName:offsetCents], range: NSRange(location:(amt?.characters.count)!-2,length:2))
    } else {
        attString.setAttributes([NSFontAttributeName:fontSuper!,NSBaselineOffsetAttributeName:offsetSymbol], range: NSRange(location:0,length:1))
        attString.setAttributes([NSFontAttributeName:fontSuper!,NSBaselineOffsetAttributeName:offsetCents], range: NSRange(location:(amt?.characters.count)!-2,length:2))
    }

    return attString
}

Hope this clarifies things, shoot me an email if you want to take the conversation off Github. sku3hh@gmail.com

vikmeup commented 8 years ago

@sinanulkuatam @simonlevy5 what's latest on this?

V-FEXrt commented 8 years ago

@simonlevy5 I need this myself. Any updates? I'll probably fork soon for my own use if not.

V-FEXrt commented 8 years ago

I have a very rough implementation of this that just forwards the account ids from plaid-ios-link to my app code. If possible I would to work with you to instead build the account id selection into plaid-ios-link and only give the selected id to my code. This would be very similar to the screenshots above.

V-FEXrt commented 8 years ago

Please see https://github.com/V-FEXrt/plaid-ios-link for reference. A change is also need for plaid-ios-sdk.

Line 207 of Plaid.m needs to be changed to

@"include_accounts": @(YES),
joaoritter commented 7 years ago

Updates anyone?

V-FEXrt commented 7 years ago

@joaoritter My fork "works" but it isn't a great solution. It has a couple places where it just throws an exception, because I don't know what to do in that case and I don't have the time to delve into it without assistance

simonlevy5 commented 7 years ago

@V-FEXrt I would be happy to assist. Please let me know where you have questions.

V-FEXrt commented 7 years ago

https://github.com/V-FEXrt/plaid-ios-link/blob/94418773f6e2878454d11c5113e1eb595cfe7d8f/PlaidLink/Classes/PLDLinkBankMFAContainerViewController.m#L154 Is the biggest one. My implementation currently just forwards an array of the account ids to the displaying code via a modified delegate.

The account ids are part of the response (https://github.com/V-FEXrt/plaid-ios-link/blob/94418773f6e2878454d11c5113e1eb595cfe7d8f/PlaidLink/Classes/PLDLinkBankMFALoginViewController.m#L85) I am just not certain on most of the control flow of the app.

simonlevy5 commented 7 years ago

@V-FEXrt I'll take a more detailed look tonight and can provide you some more detailed direction. I'm happy to look at/merge PRs into the main repo, as well.

V-FEXrt commented 7 years ago

Sounds good. I would love to make a PR once we work out the kinks, but I don't think my current progress is worth pulling into the main repo yet

joaoritter commented 7 years ago

@V-FEXrt @simonlevy5 let me know if you need another pair of eyes or a tester! Looking forward to trying this out

V-FEXrt commented 7 years ago

For anyone following this issue. We decided to switch to plaid's provided example (https://github.com/plaid/link/tree/master/examples) which recently added WKWebView.

netherlol commented 7 years ago

i'm getting null accesstoken atm with requires_recaptcha error { "request_id" = "2210d0d8-c78f-4419-87bf-d3cc4c2dcf6b"; status = "requires_recaptcha"; }

and this is plaid response:

After investigating this issue it appears that you may be using an undocumented Link API directly. We have recently rolled out changes to Link that escalate certain requests to recaptcha and your current implementation no longer works in certain scenarios.

indivisable commented 7 years ago

@sinanku @simonlevy5 Any updates on this?