I'm interested in how to implement OAuth in React using popup (window.open).
For example I have:
mysite.com — this is where I open the popup.
passport.mysite.com/oauth/authorize — popup.
The main question is how to create connection between window.open (popup) and window.opener (as it's known the window.opener is null due to cross-domain security therefore we can't use it anymore).
⇑ window.opener is removed whenever you navigate to a different host (for security reasons), there is no way around it. The only option should be doing the payment in a frame if it is possible. The top document needs to stay on the same host.
Scheme:
Possible solutions:
Check an opened window using setInterval described here.
Using cross-storage (not worth it imho ).
So what's the best recommended approach in 2019?
Wrapper for React - https://github.com/Ramshackle-Jamathon/react-oauth-popup
Suggested by Khanh TO. OAuth popup with localStorage. Based on react-oauth-popup.
Scheme:
Code:
oauth-popup.tsx:
import React, {PureComponent, ReactChild} from 'react'
type Props = {
width: number,
height: number,
url: string,
title: string,
onClose: () => any,
onCode: (params: any) => any,
children?: ReactChild,
}
export default class OauthPopup extends PureComponent<Props> {
static defaultProps = {
onClose: () => {},
width: 500,
height: 500,
url: "",
title: ""
};
externalWindow: any;
codeCheck: any;
componentWillUnmount() {
if (this.externalWindow) {
this.externalWindow.close();
}
}
createPopup = () => {
const {url, title, width, height, onCode} = this.props;
const left = window.screenX + (window.outerWidth - width) / 2;
const top = window.screenY + (window.outerHeight - height) / 2.5;
const windowFeatures = `toolbar=0,scrollbars=1,status=1,resizable=0,location=1,menuBar=0,width=${width},height=${height},top=${top},left=${left}`;
this.externalWindow = window.open(
url,
title,
windowFeatures
);
const storageListener = () => {
try {
if (localStorage.getItem('code')) {
onCode(localStorage.getItem('code'));
this.externalWindow.close();
window.removeEventListener('storage', storageListener);
}
} catch (e) {
window.removeEventListener('storage', storageListener);
}
}
window.addEventListener('storage', storageListener);
this.externalWindow.addEventListener('beforeunload', () => {
this.props.onClose()
}, false);
};
render() {
return (
<div onClick={this.createPopup)}>
{this.props.children}
</div>
);
}
}
app.tsx
import React, {FC} from 'react'
const onCode = async (): Promise<undefined> => {
try {
const res = await <your_fetch>
} catch (e) {
console.error(e);
} finally {
window.localStorage.removeItem('code'); //remove code from localStorage
}
}
const App: FC = () => (
<OAuthPopup
url={<your_url>}
onCode={onCode}
onClose={() => console.log('closed')}
title="<your_title>">
<button type="button">Enter</button>
</OAuthPopup>
);
export default App;
I once encounter an issue on my oauth login flow with window.open/window.opener bug on ms-edge
My flow before this issue was
On login button click open a popup
After successful login the oauth app redirect to my domain's page
Then i call a function of the parent window from with in the popup (window.opener.fn) with data from oauth response and the parent window then close the child popup window
My flow after this issue was
On login button click open a popup
Create a setinterval in case (window.opener is undefined)
After successful login the oauth app redirect to my domain's page
Check if window.opener is available then do #3 from the above flow and clearInterval
If window.opener is not available then since i am on my domains page i try to set localstorage and try to read the localstorage from inside the setInterval function in parent window then clear the localstorage and setInterval and proceed.
(for backward compatibility) If localstorage is also not available then set a client side cookie with the data with a short expiry (5-10 sec) time and try to read the cookie (document.cookie) inside the setInterval function in parent window and proceed.
Related
I have a generic implementation to fire a page_view google analytics event in my react application every time there's a route change:
const usePageViewTracking = () => {
const { pathname, search, hash } = useLocation();
const pathnameWithTrailingSlash = addTrailingSlashToPathname(pathname) + search + hash;
useEffect(() => {
invokeGAPageView(pathnameWithTrailingSlash);
}, [pathname]);
};
export default usePageViewTracking;
This works fine, but I need to fire ga4 page_view events with custom dimensions and if the page doesn't have some data, I should not send it in page_view event.
I turned my previous code into this:
const usePageViewTracking = () => {
const { pathname, search, hash } = useLocation();
const subscriptionsData = useAppSelector(
(state) => state?.[REDUX_API.KEY]?.[REDUX_API.SUBSCRIPTIONS]?.successPayload?.data
);
useEffect(() => {
sendPageViewEvent({ subscriptionsData });
}, [pathname, subscriptionsData]);
};
export default usePageViewTracking;
sendPageViewEvent is where I collect most of the information I need to be sent, and currently is like this:
export const sendPageViewEvent = ({ subscriptionsData }: SendPageViewEventProps): void => {
const { locale, ga } = window.appData;
const { subscriptions, merchants, providers } =
prepareSubscriptionsData({ subscriptionsData }) || {};
const events = {
page_lang: locale || DEFAULT_LOCALE,
experiment: ga.experiment,
consent_status: Cookies.get(COOKIES.COOKIE_CONSENT) || 'ignore',
...(subscriptionsData && {
ucp_subscriptions: subscriptions,
ucp_payment_providers: providers,
ucp_merchants: merchants,
}),
};
sendGA4Event({ eventType: GA4_EVENT_TYPE.PAGE_VIEW, ...events });
};
So as you can see, I have some dimensions that are always sent, and some that are conditionally sent (subscriptionsData).
The problem
The problem with this implementation is that once the page renders, it waits for subscriptionData to be available to fire the event, which would be ok, if this data would be fetched in all pages. If a page doesn't have this data, I still need to send the event, just not attach subscriptions dimensions into it.
I tried different approaches in my application, like:
going to each page and firing it individually, but since it's a huge application, it would require a huge refactoring that turns out to not to be justified for analytics purposes.❌
Having some sort of config file to define which routes fire which endpoints, but this is a terrible and unmaintainable idea ❌
Now if there would be a way to know based on the redux store how figure out which endpoints are being triggered on a page, maybe I could then detect it and decide if I should wait for this property to be available or fire the event without it.
PS: there will be more fetched data from different endpoints that I'll have to fire on page_view experiment too... and it's an SPA aplication (so everything is using CSR).
Any ideas are welcome! :D
My App which created using React Native require functionality to scan QR code with default camera app then open specific screen in the app, in order to achieve this I setup Firebase dynamic links by also using React Native Firebase library.
The setup were pretty simple, a dynamic link using Firebase provided domain, the link also contain deep link in url format https://example.page.link/abc-xyz.
After the app scan the QR it use deep link url to extract the abc-xyz part and navigate to different screen, here is my implementation.
// App.js
const handleDynamicLink = link => {
const linkCheck = new RegExp('^https://example.page.link/.*$');
let title;
if (linkCheck.test(link.url)) {
title = link.url.substring(link.url.lastIndexOf('/') + 1).split('-');
RootNavigation.navigate(Screens.offer, { title: title });
}
};
...
React.useEffect(() => {
// Handler for background/quit events
dynamicLinks().getInitialLink().then(link => {
handleDynamicLink(link);
});
// Handler for foreground events
const unsubscribe = dynamicLinks().onLink(handleDynamicLink);
return () => unsubscribe();
}, []);
// RootNavigation.js
import * as React from 'react';
export const navigationRef = React.createRef();
export function navigate(name, params) {
navigationRef.current?.navigate(name, params);
}
All necessary setup already configured both for iOS and Android, doing test with Android device by scanning the QR code, it recognise the link and navigate to intended screen, but not for iOS even though it understand the link and it only open the initial screen then stop there.
Strange thing is if I open the link directly in device browser it will open the preview page, then if I click the "open" button it open the app and navigate to target screen.
Wondering is this has something to do with navigation stuff in the iOS native side?
Turns out I need to update AppDelegate.m and add link handler for iOS.
Added below code above #end line in AppDelegate.m.
// AppDelegate.m
- (BOOL)application:(UIApplication *)application
openURL:(NSURL *)url
options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options
{
return [RCTLinkingManager application:application openURL:url options:options];
}
- (BOOL)application:(UIApplication *)application continueUserActivity:(nonnull NSUserActivity *)userActivity
restorationHandler:(nonnull void (^)(NSArray<id<UIUserActivityRestoring>> * _Nullable))restorationHandler
{
return [RCTLinkingManager application:application
continueUserActivity:userActivity
restorationHandler:restorationHandler];
}
Handling when app in foreground state or already open.
// App.js
if (Platform.OS === 'ios') {
Linking.addEventListener('url', handleDynamicLink);
}
Handling when app is fully closed/initiated by dynamic link.
// App.js
if (Platform.OS === 'ios') {
Linking.getInitialURL()
.then(link => {
handleDynamicLink({ url: link });
})
.catch(error => {
// Error handling
});
} else {
// This part for Android
dynamicLinks().getInitialLink().then(link => {
handleDynamicLink(link);
});
}
My app is requiring that google oauth (via federatedSignIn) be tapped twice in iOS devices, prior to actually signing the user in.
Process:
Upon the first tap, inapp browser opens up and you select which account you're intending to sign in with. Inapp browser closes and seems like all the rest of my logic is not being hit.
Upon the second tap, the inapp browser re-opens up again for a split second (screen is blank), and then closes and THEN the user is actually signed in.
On the iOS simulator/android, however, it seems like it works as expected. Another strange thing is that it works as expected for oauth'ing in with Apple on all devices.
Wondering if anyone else has run into this issue and if y'all have a suggestion?
Where I instantiate the hub listener:
useEffect(() => {
// NOTE: amplify hub listener
const listener = async (data: any) => {
switch (data.payload.event) {
case "signIn":
case "cognitoHostedUI":
await signInUser();
break;
case "signOut":
setUser(null);
break;
default:
break;
}
};
Hub.listen("auth", listener);
}, []);
My google oauth button component:
export function GoogleSignInButton({ title }: GoogleSignInButtonProps) {
return (
<SocialIcon
button
type="google"
title={title}
style={{ padding: 50, marginBottom: 10 }}
onPress={() =>
Auth.federatedSignIn({
provider: "Google" as any,
}).catch(federatedSignInError => {
console.log({ federatedSignInError });
throw new Error(federatedSignInError);
})
}
/>
);
}
I'm also using the react-native-inappbrowser-reborn npm package to have an internal webview when signing in, if that's relevant:
async function urlOpener(url: string, redirectUrl: string) {
await InAppBrowser.isAvailable();
const { type, url: newUrl } = (await InAppBrowser.openAuth(url, redirectUrl, {
showTitle: false,
enableUrlBarHiding: true,
enableDefaultShare: false,
ephemeralWebSession: false,
})) as RedirectResult;
if (type === "success") {
Linking.openURL(newUrl);
}
}
const appsyncAuthenticationTypeOverride = {
...config,
oauth: {
...config.oauth,
urlOpener,
},
aws_appsync_authenticationType: "AWS_IAM",
};
Amplify.configure(appsyncAuthenticationTypeOverride);
i had the same issue.
It seems to be related to Cookies in navigator, you seem to be loading the during the first logging attempt, and using the during the second one.
Also it seems to be sometimes related to redirection errors in Cognito Auth Flow.
I managed to solve it by finding this issue :
https://github.com/aws-amplify/amplify-js/issues/7468
Especially this comment :
https://github.com/aws-amplify/amplify-js/issues/7468#issuecomment-816853703
Problem
I'd like to be able to track a users location even when the app is no longer in the foreground (e.g. The user has switch to another app or switched to the home screen and locked their phone).
The use case would be a user tracking a run. They could open the app and press 'start' at the beginning of their run, then switch or minimise the app (press the home button) and lock the screen. At the end of the run they could bring the app into the foreground and press 'stop' and the app would tell them distance travelled on the run.
Question
Is tracking background geolocation possible on both iOS and Android using pure react native?
The react native docs on geolocation (https://facebook.github.io/react-native/docs/geolocation) are not very clear or detailed. The documented linked above eludes to background geolocation on iOS (without being fully clear) but does not mention Android.
Would it be best that I use Expo?
UPDATE 2019 EXPO 33.0.0:
Expo first deprecated it for their SDK 32.0.0 to meet app store guidelines but then reopened it in SDK 33.0.0.
Since, they have made it super easy to be able to implement background location. Use this code snippet that I used to make background geolocation work.
import React from 'react';
import { Text, TouchableOpacity } from 'react-native';
import * as TaskManager from 'expo-task-manager';
import * as Location from 'expo-location';
const LOCATION_TASK_NAME = 'background-location-task';
export default class Component extends React.Component {
onPress = async () => {
await Location.startLocationUpdatesAsync(LOCATION_TASK_NAME, {
accuracy: Location.Accuracy.Balanced,
timeInterval: 5000,
});
};
render() {
return (
<TouchableOpacity onPress={this.onPress} style={{marginTop: 100}}>
<Text>Enable background location</Text>
</TouchableOpacity>
);
}
}
TaskManager.defineTask(LOCATION_TASK_NAME, ({ data, error }) => {
if (error) {
alert(error)
// Error occurred - check `error.message` for more details.
return;
}
if (data) {
const { locations } = data;
alert(JSON.stringify(locations); //will show you the location object
//lat is locations[0].coords.latitude & long is locations[0].coords.longitude
// do something with the locations captured in the background, possibly post to your server with axios or fetch API
}
});
The code works like a charm. One thing to note is that you cannot use geolocation in the Expo App. However, you can use it in your standalone build. Consequently, if you want to use background geolocation you have to use this code and then do expo build:ios and upload to the appstore in order to be able to get a users background location.
Additionally, note that you must include
"UIBackgroundModes":[
"location",
"fetch"
]
In the info.plist section of your app.json file.
The Expo Team release a new feature in SDK 32 that allow you tracking in background the location.
https://expo.canny.io/feature-requests/p/background-location-tracking
Yes is possible, but not using Expo, there are two modules that I've seen:
This is a comercial one, you have to buy a license https://github.com/transistorsoft/react-native-background-geolocation
And this https://github.com/mauron85/react-native-background-geolocation
Webkit is currently evaluating a Javascript-only solution. You can add your voice here
For a fully documented proof-of-concept example please see Brotkrumen.
The most popular RN geolocation library is https://github.com/react-native-geolocation/react-native-geolocation, and it supports this quite easily. I prefer this library over others because it automatically handles asking for permissions and such, and seems to have the simplest API.
Just do this:
Geolocation.watchPosition((position)=>{
const {latitude, longitude} = position.coords;
// Do something.
})
This requires no additional setup other than including the background modes fetch and location, and also the appropriate usage descriptions.
I find this more usable than Expo's API because it doesn't require any weird top level code and also doesn't require me to do anything other than create a watch position handler, which is really nice.
EDIT 2023!:
These days I would highly recommend using Expo's library instead of any of the other community libraries (mainly because our app started crashing when android got an OS update b/c of the lib I was using).
In fact, if you have to choose between expo and non expo library, always choose the expo library if only for the stability. Setting up expo's background location watching isn't super well documented but here's what I did to get it working in our app:
import { useEffect, useRef } from "react";
import * as Location from "expo-location";
import { LatLng } from "react-native-maps";
import * as TaskManager from "expo-task-manager";
import { LocationObject } from "expo-location";
import { v4 } from "uuid";
type Callback = (coords: LatLng) => void;
const BACKGROUND_TASK_NAME = "background";
const executor: (body: TaskManager.TaskManagerTaskBody<object>) => void = (
body
) => {
const data = body.data as unknown as { locations: LocationObject[] };
const l = data?.locations[0];
if (!l) return;
for (const callback of Object.values(locationCallbacks)) {
callback({
latitude: l.coords.latitude,
longitude: l.coords.longitude,
});
}
};
TaskManager.defineTask(BACKGROUND_TASK_NAME, executor);
const locationCallbacks: { [key: string]: Callback } = {};
const hasStartedBackgroundTaskRef = {
hasStarted: false,
};
function startBackgroundTaskIfNecessary() {
if (hasStartedBackgroundTaskRef.hasStarted) return;
Location.startLocationUpdatesAsync(BACKGROUND_TASK_NAME, {
accuracy: Location.Accuracy.Balanced,
}).catch((e) => {
hasStartedBackgroundTaskRef.hasStarted = false;
});
hasStartedBackgroundTaskRef.hasStarted = true;
}
function addLocationCallback(callback: Callback) {
const id = v4() as string;
locationCallbacks[id] = callback;
return {
remove: () => {
delete locationCallbacks[id];
},
};
}
export default function useLocationChangeListener(
callback: Callback | null,
active: boolean = true
) {
const callbackRef = useRef<null | Callback>(callback);
callbackRef.current = callback;
useEffect(() => {
if (!active) return;
if (!callback) return;
Location.getLastKnownPositionAsync().then((l) => {
if (l)
callback({
latitude: l.coords.latitude,
longitude: l.coords.longitude,
});
});
startBackgroundTaskIfNecessary();
const watch = Location.watchPositionAsync({}, (location) => {
callback({
latitude: location.coords.latitude,
longitude: location.coords.longitude,
});
});
const subscription = addLocationCallback(callback);
return () => {
subscription.remove();
watch.then((e) => {
e.remove();
});
};
}, [callback, active]);
useEffect(() => {
if (__DEV__) {
addLocationCallback((coords) => {
console.log("Location changed to ");
console.log(coords);
});
}
}, []);
}
You need to ask for background location permissions before this, BTW. Follow expos guide.
It's pretty risky trusting community libraries for stuff like this because of the fact that breaking android OS updates can happen at any moment and with open source maintainers they may or may not stay on top of it (you can more or less trust expo too, though)
I have a react native app that uses webView to render an html code , i used postmessage for communicating between them. webview will send message to react native to open another react native page ( for example send message 'open login page' ) so i will recieve it in onMessage (event) , then when i want to use this.props.navigateTo it says 'can not read property navigateTo of undefined' .
i used console.log(this) and understood 'this' is DedicatedWorkerGlobalScope, that i can't use navigatTo() . how can i use navigatTo in this case (while im in DedicatedWorkerGlobalScope) or how can i close or stop rendering webview to do this?
onMessage( event ) {
let post = JSON.parse(event.nativeEvent.data);
console.log(event)
switch(post.message){
case 'open login page':
console.log(this); // 'DedicatedWorkerGlobalScope'
this.props.navigateTo('login'); //'can not read property navigation of undefined'
break;
}
}
this is the webview :
<WebView
// onNavigationStateChange={async (e) => {
// console.log(e);
// if (async() => await getKey('isLogged') === false)
// this.props.navigateTo('login') //its ok here
// }}
source={{uri: isAndroid ?'file:///android_asset/html/index.html'
:'./html/index.html'}}
ref={ webView => this.webView = webView }
onMessage={this.onMessage}
/>
i use this.props.navigatTo() for navigating and its ok but in this case state doesnt change to run this here ,
and this is my stacknavigator :
login: {
screen: Login
},
app: {
screen: App // webview defined here
},
I solve the problem myself :)) I should use bind,
OnMessage = { this.onMessage.bind (this) }
I just forgot it ...