saltedge / saltedge-examples

Examples of integrating Salt Edge API with different platforms and technologies.
https://www.saltedge.com
37 stars 49 forks source link

JavaScript onMessage callback not being triggered #71

Closed Roundups closed 3 years ago

Roundups commented 3 years ago

Hi SaltEdge,

Firstly thank you for providing this example it has really helped us create a better UX for our customers already. I'd just like to raise an issue where I am not receiving any data on the callBack method upon a successful connection. According to your example you are expecting to receive an object and use it in App.js like this:

onCallback(data) {
    // {"data":{"login_id":"111","stage":"fetching","secret":"SECRET","custom_fields":{}}}
    // {"data":{"login_id":"111","stage":"success","secret":"SECRET","custom_fields":{}}}

    this.setState({
      login: data.data,
      stage: data.data.stage
    })
  }

But according to the react-native-webview docs on the onMessage function, it must return a string. This does work if I cancel out of the SaltEdge integration and I receive the string "cancel". This is helpful as I can handle this flow in the app for my customers 👍 But the callback is not triggered at all when it is successful.

There are a couple things I have changed. I am not hitting the "https://www.saltedge.com/api/v5/connect_sessions/create" endpoint, I am hitting the partners endpoint "https://www.saltedge.com/api/partners/v1/lead_sessions/create". I don't think should matter as it also has the "javascript_callback_type": 'post_message' parameter and value.

I have also updated the code to TypeScript, ES6+ and functional components. I'll paste it below for context and I can refactor it to suit the example and open a pull request if you think it would be worth updating your example.

My App.js is OpenBanking.tsx as it is its own screen in the app:

import React, { useState } from 'react';
import { StyleSheet, Text, View, Button } from 'react-native';

import { createSELead } from '../../api/saltedge';

import { SEWebView } from '../../components/SaltEdge';

type ErrorResponse = {
    error: boolean;
    errorClass: string;
    errorMessage: string;
};

const OpenBanking = () => {
    const [connecting, setConnecting] = useState(false);
    const [connectUrl, setConnectUrl] = useState('');
    const [callBackData, setCallBackData] = useState<any>(null);
    const [errorResponse, setErrorResponse] = useState<ErrorResponse | null>(
        null
    );

    const onConnect = async () => {
        setConnecting(true);
        const data = await createSELead('an@email.address');

        if (data.error_class) {
            setErrorResponse({
                error: true,
                errorClass: data.error_class,
                errorMessage: data.error_message,
            });
        } else if (data.url) {
            setConnectUrl(data.url);
        } else {
            console.log('unknown', data);
        }
        setConnecting(false);
    };

    const onCallback = (data: any) => {
        console.log('callback data', data);
        setCallBackData(data);
    };

    const currentScreen = () => {
        if (connecting) {
            return <Text>Connecting...</Text>;
        } else if (errorResponse?.error) {
            return (
                <Text>
                    {errorResponse?.errorClass}: {errorResponse?.errorMessage}
                </Text>
            );
        } else if (callBackData?.data.stage == 'success') {
            return <Text>Connect succeeded.</Text>;
        } else if (callBackData?.data.stage == 'error') {
            return <Text>Connect failed.</Text>;
        } else if (connectUrl) {
            return <SEWebView url={connectUrl} onCallback={onCallback} />;
        } else {
            return <Button onPress={onConnect} title="Connect" />;
        }
    };

    return <View style={styles.container}>{currentScreen()}</View>;
};

export default OpenBanking;

const styles = StyleSheet.create({
    container: {
        flex: 1,
        backgroundColor: '#fff',
        alignItems: 'center',
        justifyContent: 'center',
    },
});

SaltEdge file:

import React from 'react';
import { StyleSheet, Dimensions } from 'react-native';
import { WebView } from 'react-native-webview';

export const SEWebView = ({
    url,
    onCallback,
}: {
    url: string;
    onCallback: (data: any) => void;
}) => {
    const onMessage = (event: any) => {
        console.log('on message event', event);
        if (!onCallback) {
            return;
        }

        // action for user cancelling integration with SE
        if (event.nativeEvent.data === 'cancel') {
            // add to useContext
            console.log('CANCELLED');
        }

        if (event.nativeEvent.data !== 'cancel') {
            onCallback(JSON.parse(event.nativeEvent.data));
        }
    };

    return (
        <WebView
            style={styles.container}
            source={{ uri: url }}
            onMessage={(event) => onMessage(event)}
        />
    );
};

const styles = StyleSheet.create({
    container: {
        flex: 1,
        width: Dimensions.get('window').width,
        height: Dimensions.get('window').height,
    },
});

The createSELead function calls an endpoint that handles the SE API call. Its in python and I can paste it here if required.

I am using expo SDK 42.0.4 and react-native-webview 11.6.2.

Thanks. Let me know if you require any more information.

alisnic commented 3 years ago

Hi @Roundups! Please give us a few days to investigate and test. Will reply here soon

Roundups commented 3 years ago

Hi @alisnic, any updates on this? Anything I can help with?

baller784 commented 3 years ago

Hi, @Roundups. We have tested with same versions (expo SDK 42.0.4 and react-native-webview 11.6.2). On our side everything works fine, we receive final success callbacks:

Screenshot 2021-10-04 at 14 51 18

Could you please paste createSELead function here so we could check your request?

Roundups commented 3 years ago

Thanks @baller784, is your pasted response coming from the console.log in the onCallback function?

Here is my createSELead api function:

export const createSELead = async (email) => {
    try {
        const response = await fetch(`${USER_ENDPOINT}/create-saltedge-customer/`, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
            },
            body: JSON.stringify({
                email,
                // kyc
            }),
        });

        if (!response.ok) {
            throw new Error('Network response was not ok');
        }
        return response.json();
    } catch (e) {
        console.error('SaltEdge lead create error', e);
    }
};

This fetch request hits an endpoint on our backend that creates a new SE lead hitting this endpoint https://www.saltedge.com/api/partners/v1/leads with the provided email.

Upon a successful response we then hit another SE endpoint https://www.saltedge.com/api/partners/v1/lead_sessions/create with this payload:

{
    "data": {
        "customer_id": "123",
        "javascript_callback_type": "post_message",
        "daily_refresh": True,
        "regulated": True,
        "include_fake_providers": False,
        "allowed_countries": ["GB", "IE"],
        "consent": {
            "from_date": "2021-10-05",
            "period_days": 90,
            "scopes": ["account_details", "transactions_details"]
        },
        "attempt": {
            "from_date": "2021-10-05",
            "return_to": "exp://192.168.1.202:19000/--/openBankingSuccess/"
        },
        "return_connection_id": True
    }
}

Upon a successful response we return the provided URL from SE to the app which is then used as the connectUrl. This all seemingly works fine, I can go through the process of connecting my account but when I am returned to the app I don't receive a callback message.

I am able to carry on the flow as I can pick up the connection_id from the route params using react-navigation but I would have much more confidence using the callback method as I can handle other responses from the SE flow too, such as errors.

baller784 commented 3 years ago

@Roundups yes, the pasted response is coming from onCallback function.

Also, what do you mean by

but when I am returned to the app I

Can you also maybe provide a video recording so that we will understand the user flow?

Roundups commented 3 years ago

Hi @baller784, attached is a video of me linking monzo using the above code. You can see the WebView renders correctly inside the app and all the redirects work. I am spitting out the connection ID on the success screen as I can grab it from react-navigation props. I would have included the console too but as I don't receive a response there is nothing to console log! I hope this helps, let me know if there is any more I can add.

What I mean by "but when I am returned to the app I" is when SE returns me to the Expo Go app (this would be the Roundups app in production). I assume this is when the callback is triggered? Maybe not? I guess the cancel callback gets triggered from the WebView so I need to redirect the user from SE in safari back to the app but on the same screen as the WebView to trigger the callback?

https://user-images.githubusercontent.com/61463286/136179405-6ff92a1e-9d7a-4be1-b26c-996d33bb1b27.mp4

baller784 commented 3 years ago

@Roundups I see that your flow is working fine. I think, that when you return to the app, you should extract the received parameters from a deep link, which is received by your application.

https://blog.jscrambler.com/how-to-handle-deep-linking-in-a-react-native-app https://reactnative.dev/docs/linking

alisnic commented 3 years ago

@Roundups expanding on @baller784 reply, it makes sense that onCallback is not triggered. This function is called when Salt Edge Connect sends postMessage to parent frame. Therefore, when Connect is in WebView all is fine (at the beginning of the video). However we can see from video that you came back to OS browser from Monzo app (not to the original WebView), hence there is no parent frame to notify

The connection_id should be present as a query string in the deep link that is the final redirect

Roundups commented 3 years ago

Thanks @alisnic, so this is desired behaviour? As you can see Monzo app redirects me to OS browser. Would other banking apps redirect back to my app? Can I have control over this? What bank did you test with that triggered the callback? I can add fakeproviders to test with if required.

The connection_id is present and I can complete the link using that - thank you. It's just a shame that if there was an error or other data I would like to receive from SE, I won't be able to get it in this specific example.

In any case in this flow the callback is not triggered, which is manageable I just want to confirm that I have implemented the example correctly and this unfortunately is a limitation with some banking apps.

alisnic commented 3 years ago

@Roundups this is not due to the Bank. Salt Edge does all the OAuth exchange with the Bank, that's why we pass Salt Edge Connect as a return url to Bank. After we do all the OAuth exchange, only then we return to your app (return_to).

If you want to know whether there was an error, all you have to do is to pass return_error_class: true to https://docs.saltedge.com/partners/v1/#lead_sessions-create. If you do that, in case there was an error during connection process, it will be put as a query string to your deep link alongside connection_id

Additionally, if you need more info/context, you can use that connection_id to make a request to https://docs.saltedge.com/partners/v1/#connections-show and inspect the state of connection

So answering your question, you did the integration correct. You just need the final piece I described above.

Roundups commented 3 years ago

I will add the return_error_class parameter and work from there. Thanks @alisnic and @baller784 for confirming.