How to configure React Native Linking? - javascript

I am using deep linking for handling notifications in my app. For remote notifications I use React Native Firebase and for local notifications I use react-native-push-notification. I am handling background and quit state notifications with react-navigation's deep linking system, but for foreground notifications I use react-native's Linking and this is where the problem is. Linking.canOpenUrl returns supported but Linking.openUrl does nothing. I already added LSApplicationQueriesScheme key to my info.plist and added my prefix to it.
this is for foreground handling =>
const onOpenNotification = async (data) => {
const messagesObj = await AsyncStorage.getItem('messages')
const messages = JSON.parse(messagesObj)
const notificationData = messages[data.id]
Linking.canOpenURL(notificationData.link).then(supported => {
if (supported) {
console.log("supported!")
Linking.openURL(notificationData.link)
}
else {
console.log("Cannot open! ")
}
}).catch(err => console.log(err))
}
this is for background and quit state handling =>
export const notificationLinking = {
prefixes: ['rateet://app'],
config,
async getInitialUrl(){
// Check if app was opened from a deep link
const url = await Linking.getInitialURL();
if (url != null) {
return url;
}
// Check if there is an initial firebase notification
const message = await messaging().getInitialNotification();
// Get the `url` property from the notification which corresponds to a screen
// This property needs to be set on the notification payload when sending it
return message?.data.link;
},
subscribe(listener){
const onReceiveURL = (url) => listener(url);
// Listen to incoming links from deep linking
Linking.addEventListener('url', onReceiveURL);
// Listen to firebase push notifications
const unsubscribeNotification = messaging().onNotificationOpenedApp(
(message) => {
const url = message.data.link;
if (url) {
// Any custom logic to check whether the URL needs to be handled
//...
// Call the listener to let React Navigation handle the URL
listener(url);
}
}
);
return () => {
// Clean up the event listeners
Linking.removeEventListener('url', onReceiveURL);
unsubscribeNotification();
}
}
}

Related

web3js intercepting when metamask connects manually

Currently I have the following code to connect the metamask wallet in the browser (using reactjs and web3js):
function App() {
const [contract, setContract] = useState();
const [account, setAccount] = useState();
const[web3Obj, setWeb3Obj]=useState();
useEffect(()=>{
async function load(){
const web3 = new Web3(Web3.givenProvider || 'http://http://localhost:7545');
setWeb3Obj(web3);
const accounts = await web3.eth.requestAccounts();
console.log(accounts[0] + " is the account");
setAccount(accounts[0]);
$.get( "getcontractaddress")
.done( function( data ) {
const _contract = new web3.eth.Contract(abi, data);
_contract.address = data;
console.log(_contract.address + " is the contract");
setContract(_contract);
});
}
load();
},[])
return(
);
}
I deleted the return(); part from the snippet because its irrelevant to the question.
Whenever I switch to a different account in metamask, the "account" object is not being updated. How can I intercept the event of metamask switching accounts to automatically reset the account object to the new account?
Your component does not know when you switch accounts in Metamask. To detect account changes, Metamask provides the accountsChanged event.
ethereum.on('accountsChanged', handler: (accounts: Array<string>) => void);
You can add the event to your load function or make another useEffect. You can also remove the event when your component unmounts using ethereum.removeListener.
useEffect(() => {
const { ethereum } = window;
if (ethereum && ethereum.on) {
const handleAccountsChanged = (accounts) => {
console.log("accountsChanged", accounts);
setAccount(accounts[0])
};
ethereum.on('connect', handleConnect);
return () => {
if (ethereum.removeListener) {
ethereum.removeListener('accountsChanged', handleAccountsChanged);
}
};
}
}, []);

Using a service worker to redirect

I have a use case with my service worker where on certain occasions I need to either reload the page, or redirect the page.
We currently use the service worker as an interceptor for all navigation fetches. Because of this it's important to reload the page when a service worker is activated. I currently do this in the activated event as such:
function getClientList(): Promise<readonly WindowClient[]> {
return self.clients.claim().then(() =>
self.clients.matchAll({
type: 'window'
})
);
}
self.addEventListener('activate', async (event: ExtendableEvent) => {
console.log('Service Worker activated');
// we need to reload the page here so that the navigation fetch request is caught
const clientList = await getClientList();
for (const client of clientList) {
const url = new URL(client.url);
await client.navigate(`${url.pathname}${url.search}`);
}
});
This seems to be working without any issues, I am just concerned as to whether this is the best approach.
The second condition is that I need to redirect the user when the url in a service worker's navigation fetch event has a certain query param in it.
async function handleNavigationRequest(request: Request, workerLocation: WorkerLocation): Promise<Response> {
const requestURL = new Url(request.url)
const lid = requestURL.searchParams.get('lid');
const code = requestURL.searchParams.get('code');
const optin = requestURL.searchParams.get('optin');
const dpp = !!rbdsCode && !!loyaltyId && !!optin;
if (dppRequest) {
const clientList = await getClientList();
for (const client of clientList) {
const url = new URL(`https://some-domain.com/#/getTokens?brand=${BRAND}&code=${code}&lid=${loyaltyId}&optin=${optin}&env=${DPP_SSO_ENV}`);
await client.navigate(url);
}
}
}
self.addEventListener('fetch', (event) => {
if (event.request.method === 'GET' && event.request.mode === 'navigate') {
event.respondWith(
handleNavigationRequest(event.request, self.location)
);
}
});
This second part is partially working. It seems the first time the service worker is activated, it reloads the page as expected in step 1, and then picks up the correct query params in step 2 and performs the redirect.
However if I simply run a request with the same query params on an instance where the service worker has already been activated, I get an error in my console stating...
Uncaught (in promise) TypeError: Cannot navigate to URL: https://some-domain.com/#/getTokens?brand=someBrand&code=572896&lid=7Rewards&optin=Y&env=dev

Unnecessary parameter in useEffect dependency array

I'm creating an application where users can create and share notes.
To share each other's notes users have to send requests to specific users.
The requests are fetched whenever home is loaded.
However, requests is a context since it is also consumed in the toolbar and requests page to show the presence of the requests
When I'm using setRequsts method of the context to set all the requests after home loads, the fetch goes into an infinite loop of /noteand /me URLs, since the setRequests method is also provided in the dependency array of useEffect
When removed, useEffect show missing dependencies. What's the work around?
const {setRequests } = useContext(RequestsContext)
const [notes, setNotes] = useState([])
const [fetched, setFetched] = useState('')
const { isAuthenticated } = props
const {page}=useContext(PageContext)
const [sortBy,setSortBy]=useState('latest')
useEffect(() => {
const fetch = async () => {
try {
let url = 'http://192.168.56.1:5000/api/v1/note', p, sort
if (page) p = `?page=${page}&limit=12`
if (sortBy === 'latest') {
sort=''
} else if (sortBy === 'most_liked') {
sort='&sort=likes'
}
const res = await Axios.get(url+p+sort)
setNotes(res.data.data)
if (res.data.data.length > 0) {
setFetched('Y')
} else {
setFetched('N')
}
} catch (err) {
console.log(err)
} finally {
if (isAuthenticated) {
const fetch = async () => {
const res = await axios.get(`user/me`)
if (res.data.data.createdPosts.length > 0) {
const arr = res.data.data.createdPosts.map(el => el.request)
console.log(arr)
setRequests(arr)
}
}
fetch()
}
}
}
fetch()
}, [isAuthenticated, /* setRequests, */ page, sortBy])
The problem is that the context provides a technically different setRequests function on each render (that have a different address). This causes useEffect to fire on each render.
To work around this, you could wrap setRequests in a useCallback() hook, like so:
// ...
const wrappedSetRequests = useCallback(setRequests, []);
// ...
useEffect(() => {
// do your stuff using 'wrappedSetRequests' instead of setRequests.
}, [ wrappedSetRequests /*...*/ ]);

ServiceWorker claiming late in the lifecycle to use client.navigate in notificationclick event handler

I have a firebase serviceworker that shows notifications when a message is pushed from Firebase Cloud Messaging (FCM).
It also publishes a post so that my React App can update accordingly.
/* eslint-env worker */
/* eslint no-restricted-globals: 1 */
/* global firebase */
/* global clients */
import config from './config'
importScripts('https://www.gstatic.com/firebasejs/4.8.1/firebase-app.js')
importScripts('https://www.gstatic.com/firebasejs/4.8.1/firebase-messaging.js')
const { FIREBASE_MESSAGING_SENDER_ID } = config
firebase.initializeApp({ messagingSenderId: FIREBASE_MESSAGING_SENDER_ID })
const messaging = firebase.messaging()
messaging.setBackgroundMessageHandler(payload => {
const title = payload.data.title
const options = {
body: payload.data.body,
icon: payload.data.icon,
data: payload.data,
}
clients.matchAll({ includeUncontrolled: true }).then(clientz => {
clientz.forEach(client => {
sendMessageToClient(client, 'NEW_USER_NOTIFICATON')
})
})
return self.registration.showNotification(title, options)
})
const sendMessageToClient = (client, message) => {
const messageChannel = new MessageChannel()
client.postMessage(message, [messageChannel.port2])
}
This all works fine, but I have added it for context.
What I want to do is have a click function that focuses on the correct window/tab and navigates to a link that is passed to it. Or if the tab is not open, open a new window and go to the link.
This is the code I have so far, added to the above file.
self.addEventListener('notificationclick', event => {
const clickedNotification = event.notification
const link = clickedNotification.data.link
clickedNotification.close()
const promiseChain = self.clients.claim()
.then(() => self.clients
.matchAll({
type: 'window',
})
)
.then(windowClients => {
let matchingClient = null
windowClients.forEach(client => {
if (client.url.includes(matching_url)) {
matchingClient = client
}
})
if (matchingClient) {
return matchingClient.navigate(link)
.then(() => matchingClient.focus())
}
return clients.openWindow(link)
})
event.waitUntil(promiseChain)
})
So, I realise that the chained navigate and focus inside a then is probably bad practice, but for now, I am just trying to get it to work. Then I will try and come up with a clever solution.
So the problem with my code is that the clients.claim() doesn't seem to be working. The matchAll doesn't return anything to the next then, the argument is an empty array.
I could simply add the includeUncontrolled: true option to the matchAll, but the navigate command only works on a controlled client instance.
If I try the often referenced Google example for claiming and navigation, it works fine:
self.addEventListener('activate', event => {
event.waitUntil(self.clients.claim().then(() => {
// See https://developer.mozilla.org/en-US/docs/Web/API/Clients/matchAll
return self.clients.matchAll({type: 'window'});
}).then(clients => {
return clients.map(client => {
// Check to make sure WindowClient.navigate() is supported.
if ('navigate' in client) {
return client.navigate('activated.html');
}
});
}));
});
So I am stuck.
The serviceworker is activated immediately, so I assume that it claim a client at any point after that.
Have I fallen for a random ServiceWorker Gotcha?
Can the claim only be used and navigated to on the handling of an activation event?
I would appreciate any help available.
Cheers
I couldn't get this to work.
But I thought it would be worth documenting my workaround.
I could not get client.navigate to work in the notificationclick event handler.
So instead I just sent a postMessage containing the URL to be picked up in my app to trigger the redirect there, without any client claiming anywhere.
self.addEventListener('notificationclick', event => {
const clickedNotification = event.notification
const link = clickedNotification.data.link
clickedNotification.close()
const promiseChain = self.clients.matchAll({
type: 'window',
includeUncontrolled: true,
})
.then(windowClients => {
let matchingClient = null
windowClients.forEach(client => {
if (client.url.includes(matching_url)) {
matchingClient = client
}
})
if (matchingClient) {
sendMessageToClient(matchingClient, { type: 'USER_NOTIFICATION_CLICKED', link })
return matchingClient.focus()
}
return clients.openWindow(link)
})
event.waitUntil(promiseChain)
})
const sendMessageToClient = (client, message) => {
const messageChannel = new MessageChannel()
client.postMessage(message, [messageChannel.port2])
}

React-native open link in browser and return to app

I've developing an app in react-native that should communicate with a gateway for payments, after finishing the payment process (success or failure) I need to show an alert to user. For this purpose, I open a link in WebView and after that I get return's url with onNavigationStateChange and show success or failure message.
But, this flow for security issues must be done in a default device browser.
Current Code:
const BASEURL = 'https://gatewayURL/?ID=';
let Token = null;
let paymentAccepted = null;
let paymentFactorId = null;
class Gateway extends PureComponent {
static propTypes = {
dispatch: PropTypes.func,
navigation: PropTypes.any,
}
componentWillMount() {
this.props.dispatch(getPaymentStatus());
}
_onLoad(webViewState) {
let url = webViewState.url.toString();
let isResponseValid = url.includes('backFromGateway');
if(isResponseValid){
if(this.props.checkedPaymentStatus != 'checked' ){
setTimeout(() => {
this.props.dispatch(setPaymentStatus('checked'));
let splitedURL = url.split("/");
paymentFactorId = splitedURL[splitedURL.length -2];
if(splitedURL[splitedURL.length - 1] === '0'){
paymentAccepted = true;
this.props.dispatch(setGatewayResponse('done', paymentFactorId));
}
else {
paymentAccepted = false;
this.props.dispatch(setGatewayResponse('rejected', paymentFactorId));
}
this.props.navigation.navigate('BackFromGateway', { title: '' })
}, 1000);
}
}
}
render() {
const { addNewOrderGatewayToken, checkedPaymentStatus } = this.props;
token = addNewOrderGatewayToken;
let view = null;
if(checkedPaymentStatus !== 'checked'){
view = <WebView onNavigationStateChange={this._onLoad.bind(this)} style={styles.container} source={{ uri: `${BASEURL}${token}` }}/>
}
else{
view = <View></View>
}
return (
<View style={styles.container}>
{view}
</View>
);
}
}
Any idea?
Thanks
If you can make callbacks from the gateway website, then I recommend to use deep linking to handle flow between app and browser. Basically, your app will open the gateway website for payment, and depending on payment result, the website will make a callback to the app using its deep link. App then will listen to the link, take out necessary information and continue to proceed.
What you need to do is:
Set up deep linking in your app. You should follow the guide from official website (here) to enable it. Let pick a random URL here for linking, e.g. gatewaylistener
Set the necessary callbacks from gateway to your app. In your case, since you need to handle successful payment and failed payment, you can add 2 callbacks, e.g. gatewaylistener://success?id={paymentId} and gatewaylistener://error?id={paymentId}
Finally, you need to listen to web browser from the app. One way to do that is add listener right inside the component opening the gateway.
// setup
componentDidMount() {
Linking.getInitialURL().then((url) => {
if (url) {
this.handleOpenURL(url)
}
}).catch(err => {})
Linking.addEventListener('url', this.handleOpenURL)
}
componentWillUnmount() {
Linking.removeEventListener('url', this.handleOpenURL)
}
// open your gateway
async openGateWay = () => {
const { addNewOrderGatewayToken } = this.props
const url = `${BASEURL}${addNewOrderGatewayToken}`
const canOpen = await Linking.canOpenURL(url)
if (canOpen) {
this.props.dispatch(setPaymentStatus('checked'))
Linking.openURL(url)
}
}
// handle gateway callbacks
handleOpenURL = (url) => {
if (isSucceedPayment(url)) { // your condition
// handle success payment
} else {
// handle failure
}
}
For authentication purposes, using a deep linking redirection for example, you can use an embedded browser with Chrome Custom Tabs from Android and SafariViewController from iOS, check the InAppBrowser component to support both platforms with the same code (Linking is already used internally to detect the deep link redirection).
As you can see from the example folder, you can use a custom deep link configured from your app (AndroidManifest for Android and Info.plist for iOS):
getDeepLink (path = '') {
const scheme = 'my-demo'
const prefix = Platform.OS === 'android' ? `${scheme}://demo/` : `${scheme}://`
return prefix + path
}
async tryDeepLinking () {
const redirectToURL = `https://proyecto26.github.io/react-native-inappbrowser/`
const redirectUrl = this.getDeepLink('home')
const url = `${redirectToURL}?redirect_url=${encodeURIComponent(redirectUrl)}`
try {
if (await InAppBrowser.isAvailable()) {
const result = await InAppBrowser.openAuth(url, redirectUrl)
await this.sleep(800)
Alert.alert('Response', JSON.stringify(result))
} else {
// You can use Linking directly for iOS < 9
}
} catch (error) {
Alert.alert('Something’s wrong with the app :(')
}
}

Categories

Resources