Linking.getInitialURL() is not being cleared after used for deeplink - android

I've had this problem for like 2 weeks. I used Wix's Navigation for navigating around the app. I followed this tutorial for implementing the deeplink/universal link.
I have a base class called BaseScreen where I keep all the deeplink handler like in the tutorial. This BaseScreen would looks like this:
componentDidMount(){
// this handles the case where the app is closed and is launched via Universal Linking.
Linking.getInitialURL()
.then((url) => {
if (url) {
// Alert.alert('GET INIT URL','initial url ' + url)
this.resetStackToProperRoute(url)
}
})
.catch((e) => {})
// This listener handles the case where the app is woken up from the Universal or Deep Linking
Linking.addEventListener('url', this.appWokeUp);
}
componentWillUnmount(){
// Remove the listener
Linking.removeEventListener('url', this.appWokeUp);
}
appWokeUp = (event) => {
// this handles the use case where the app is running in the background and is activated by the listener...
// Alert.alert('Linking Listener','url ' + event.url)
this.resetStackToProperRoute(event.url)
}
resetStackToProperRoute = (url) => {
// grab the trailing portion of the url so we can use that data to fetch proper information from the server
let trailing = url.slice(url.lastIndexOf('=') + 1, url.length)
// go to the desired screen with the trailing token grabbed from the url
this.props.navigator.resetTo({
screen: 'NewPassword',
overrideBackPress: true,
passProps: {
token: trailing
},
animated: true,
animationType: 'fade',
navigatorStyle: {
navBarHidden: true,
}
})
}
When the app launch, it'll show the screen LoginScreen which extends the BaseScreen above. After killing the app, click the url from the mail, the app launches LoginScreen first, then it'll redirect to the screen NewPassword, and after everything has done, I'll redirect back to LoginScreen by:
this.props.navigator.resetTo({
screen: 'LoginScreen',
animated: true,
overrideBackPress: true,
animationType: 'fade',
navigatorStyle: {
navBarHidden: true,
}
})
But the Linking.getInitialURL() of the LoginScreen still receive the old url, so it'll redirect to NewPassword again, and it's a loop.
I've also tried to pass: passProps: {} option when resetTo the LoginScreen but no luck.
I guess the only way to fix it is to clear the initialUrl manually after everything's done in NewPassword screen. The listener for the BaseScreen should be there because if I don't kill the app (just minimize it), the listener should be running to navigate to NewPassword.
Wix's navigation has a doc for Deeplink, I tried putting method onNavigatorEvent(event) into the BaseScreen but it doesn't get called. I don't know if I miss something.
Thank you for your time. Any idea would be appreciated

Linking.getInitialURL() gives us the same Url when we come back to the same page again, to Overcome this we can do a simple condition of not to call the DeepLink function. Something like...
Step 1: First init a dummyDeepLinkedUrl String .
var dummyDeepLinkedUrl;
Step 2: Check for the condition like, if deeplinkUrl is coming from Linking.getInitialURL() and deeplinkUrl is not equal to the dummyDeepLinkedUrl .
if (url && url != dummyDeepLinkedUrl) {}
Step 3: If not same call the Deeplink Function and assign the deeplinkUrl to dummyDeepLinkedUrl.
this.navigateToRespectivePage(url);
dummyDeepLinkedUrl = url;
Finally this will look like :
Linking.getInitialURL().then(url => {
if (url && url != dummyDeepLinkedUrl) {
this.navigateToRespectivePage(url);
dummyDeepLinkedUrl = url;
}
});

There are two ways to handle URLs that open your app.
If the app is already open, the app is foregrounded and a Linking event is fired You can handle these events with
Linking.addEventListener(url, callback).
If the app is not already open, it is opened and the url is passed in as the initialURL You can handle these events with
Linking.getInitialURL(url) -- it returns a Promise that resolves to
the url, if there is one.
You can read more detail
Here is the example
export default class App extends Component {
constructor(props) {
super(props)
this.state = {
initialised: false
}
}
componentDidMount() {
AppState.addEventListener('change', this._handleAppStateChange);
Linking.addEventListener('url', event => {
console.log('deep link from background', event.url)
})
}
_handleAppStateChange = async (nextAppState) => {
const url = await Linking.getInitialURL();
if (url !== null && !this.state.initialised) {
this.setState({ initialised: true })
console.log('deep link from init app', url)
}
}
componentWillUnmount() {
AppState.removeEventListener('change', this._handleAppStateChange);
Linking.removeEventListener('url')
}
}

Related

React PWA application blank page when open it with android/ios

After creating a PWA with react every thing works perfectly except when i want to open the app with a mobile device android/ios....it displays a white screen with no informations.
/////
The app is deployed with IIS manager
my service **service-worker.ts
**
`
/// <reference lib="webworker" />
/* eslint-disable no-restricted-globals */
// This service worker can be customized!
// See https://developers.google.com/web/tools/workbox/modules
// for the list of available Workbox modules, or add any other
// code you'd like.
// You can also remove this file if you'd prefer not to use a
// service worker, and the Workbox build step will be skipped.
import { clientsClaim } from 'workbox-core';
import { ExpirationPlugin } from 'workbox-expiration';
import { precacheAndRoute, createHandlerBoundToURL } from 'workbox-precaching';
import { registerRoute } from 'workbox-routing';
import { StaleWhileRevalidate } from 'workbox-strategies';
declare const self: ServiceWorkerGlobalScope;
clientsClaim();
// Precache all of the assets generated by your build process.
// Their URLs are injected into the manifest variable below.
// This variable must be present somewhere in your service worker file,
// even if you decide not to use precaching. See https://cra.link/PWA
precacheAndRoute(self.__WB_MANIFEST);
// Set up App Shell-style routing, so that all navigation requests
// are fulfilled with your index.html shell. Learn more at
// https://developers.google.com/web/fundamentals/architecture/app-shell
const fileExtensionRegexp = new RegExp('/[^/?]+\\.[^/]+$');
registerRoute(
// Return false to exempt requests from being fulfilled by index.html.
({ request, url }: { request: Request; url: URL }) => {
// If this isn't a navigation, skip.
if (request.mode !== 'navigate') {
return false;
}
// If this is a URL that starts with /_, skip.
if (url.pathname.startsWith('/_')) {
return false;
}
// If this looks like a URL for a resource, because it contains
// a file extension, skip.
if (url.pathname.match(fileExtensionRegexp)) {
return false;
}
// Return true to signal that we want to use the handler.
return true;
},
createHandlerBoundToURL(process.env.PUBLIC_URL + '/index.html')
);
// An example runtime caching route for requests that aren't handled by the
// precache, in this case same-origin .png requests like those from in public/
registerRoute(
// Add in any other file extensions or routing criteria as needed.
({ url }) =>
url.origin === self.location.origin && url.pathname.endsWith('.png'),
// Customize this strategy as needed, e.g., by changing to CacheFirst.
new StaleWhileRevalidate({
cacheName: 'images',
plugins: [
// Ensure that once this runtime cache reaches a maximum size the
// least-recently used images are removed.
new ExpirationPlugin({ maxEntries: 50 }),
],
})
);
// This allows the web app to trigger skipWaiting via
// registration.waiting.postMessage({type: 'SKIP_WAITING'})
self.addEventListener('message', (event) => {
if (event.data && event.data.type === 'SKIP_WAITING') {
self.skipWaiting();
}
});
// Any other custom service worker logic can go here.
const CACHE_NAME = 'cache_sample';
const urlsToCache = ['index.html', 'offline.html'];
const version = 'v0.0.1';
//install sw at first time
//place to cache assets to speed up the loading time of web page
self.addEventListener('install', (event: any) => {
console.log('sw install event');
event.waitUntil(
caches.open(version + CACHE_NAME).then((cache) => {
console.log('opened cache');
return cache.addAll(urlsToCache);
})
);
});
//Activate the sw after install
//Place where old caches are cleared
self.addEventListener('activate', (event: any) => {
console.log('sw activate event');
event.waitUntil(
caches.keys().then((cacheNames) =>
Promise.all(
cacheNames
.filter((cacheName) => {
return cacheName.indexOf(version) !== 0;
})
.map(function (cachName) {
return caches.delete(cachName);
})
)
)
);
});
//listen for requests
self.addEventListener('fetch', (event: any) => {
event.respondWith(
caches.match(event.request).then((response) => {
return response || fetch(event.request);
})
);
});
// Any other custom service worker logic can go here.
`
PS : My start_url : "/" in the manifest.json and i've already set the basename attribute in my Router

How to trigger a service from one component to dismiss a popup in another component [angular]

For context, we've opted to use the angular router instead of the ionic navigation controller to handle routing.
I'm trying to handle navigation using the android back button. Pressing it when using an ionic framework emits global-level ion-back-button events. Since we're not using the ionic navigation controller, the only trigger to emit those events is pressing the android hardware back button. I've created a global listener for ion-back-button events, and inside of that listener I use a navigation service to handle page to page navigation. What that handler needs to do, which I'm having a hard time with, is it also needs to be able to check whether it's necessary to close popups before attempting page navigation.
I can't have higher priority back button listeners in the popup modals in the angular components because then the global back button listener would never fire.
This is what the handler in the app component looks like:
backButtonEvent() {
this.platform.backButton.subscribeWithPriority(10, () => {
if (!this.modalService.remove()) { // what should happen here is checking whether a popup exists. If the remove function returns no, then proceed to page navigation handling.
const backUrl = this.backService.getBackURL();
if (
!backUrl.includes("dataload") /*previous page is not dataload*/ &&
!backUrl.includes("login") /*previous page is not login */ &&
!this.navCtrl.url.includes("dataload") /*current page is not dataload */ &&
!this.navCtrl.url.includes("login") /*current page is not login */
) {
this.navCtrl.navigateByUrl(backUrl);
}
}
});
}
And here is what most modals in the app look like:
async showInformationModal() {
const modalData: BasicModalData = {
generic text: "text"
};
const infoModal = await this.modalController.create({
component: BasicModalComponent,
componentProps: {
modalData
},
backdropDismiss: true,
showBackdrop: true,
cssClass: "modalClass"
});
return infoModal.present();
}
What might make the most sense is creating a modal service with a function that can emit a "dismissed: true" property to assign to the infoModal. I'm not sure how to do that.
The most elegant way is to use a GuardService and Routes "canDeactivate" attribute.
navigation_guard_service.ts:
#Injectable({
providedIn: 'root'
})
export class NavigationGuardService implements CanActivate, CanDeactivate<CUSTOM_OBJECT> {
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean|UrlTree { /*nothing here for now */ }
canDeactivate(component: CUSTOM_OBJECT, currentRoute: ActivatedRouteSnapshot, currentState: RouterStateSnapshot, nextState?: RouterStateSnapshot): boolean | UrlTree | Observable<boolean | UrlTree> | Promise<boolean | UrlTree> {
/*
1. use "currentRoute" and/or "currentState" to recognize which is your current Page
2. call your "Page.onDeactivate() : boolean" method you have previous created in that Page
3. (here) return TRUE if you allow navigation to leave current URL, or FALSE to denied navigation (a 3rd possible return value is an "UrlTree" to change navigation to a different URL)
*/
}
}
In Page's Module file:
#NgModule({
imports: [RouterModule.forChild(
[
{ path: '', children: [
{ path: ROUTE_LOGIN, loadChildren: () => import('./login/login.module').then(m => m.ChooserPageModule)/*, canActivate: [NavigationGuardService]*/, canDeactivate: [NavigationGuardService] },
{ path: '', redirectTo: ROUTE_LOGIN, pathMatch: 'full' },
]},
{ path: '', redirectTo: ROUTE_AUTH_LOGIN, pathMatch: 'full' },
{ path: '**', redirectTo: ROUTE_AUTH_LOGIN }
]
)],
exports: [RouterModule],
})

How to finish current component while navigating to next component in react native

Hi I am trying to navigate to next component using navigate function. I am using react-navigation for the navigation among multiple components.
Suppose I have index.android.js and DashboardScreen.js component. I am trying to navigate to DashboardScreen.js component from index component.
It is navigating but index component always retain in component stack. when I press back then it opens index.android.js which should not be. Does anyone know how to manage this in react-native. In Android, finish() works for this.
navigate("DashboardScreen");
When I am navigating from SplashScreen to EnableNotification then SplashScreen should be destroyed, if I am navigating from EnableNotification to CreateMessage then EnableNotification should be destroyed and if I am navigating from CreateMessage to DashboardScreen then CreateMessage should be destroyed. As of now no component is being destroyed.
index.android.js
class SplashScreen extends Component {
render() {
if (__DEV__) {
console.disableYellowBox = true;
}
const { navigate } = this.props.navigation;
AsyncStorage.getItem("#ProductTour:key").then(value => {
console.log(value);
if (value) {
navigate("DashboardScreen");
}
});
return (
....
);
}
}
const App = StackNavigator(
{
Splash: {
screen: SplashScreen,
navigationOptions: {
header: {
visible: false
}
}
},
EnableNotification: {
screen: EnableNotificationScreen,
navigationOptions: {
header: {
visible: false
}
}
},
CreateMessage: {
screen: CreateMessageScreen,
navigationOptions: {
header: {
visible: false
}
}
},
DashboardScreen: {
screen: DashboardScreen,
navigationOptions: {
header: {
visible: false
}
}
}
},
{
initialRouteName: "Splash"
}
);
Just use 'replace' in place of 'navigate'
this.props.navigation.replace('Your Next Component Name')
First of all, using AsyncStorage in an a synchronous function (most especially a lifecycle one) is such a bad idea. You should typically keep ASyncStorage to places in your folder / app structure that make sense for where you access/keep data but since that's not the question I will just mention it quickly here...
Basically you are asking to navigate once the ASync method completes itself based on EVERY render... Those new to RN should know that an awful lot of things can cause a render to fire. Some cases, the render function can fire (I have seen this many times before) 10 or more times before finalizing the last render. This means you would have fired that ASyncStorage method 10 times... definitely something to think about when implementing this stuff. So more or less, the .then(); part of the AsyncStorage function is firing long after the render has already finished doing it's thing. If it was a reasonable approach to use I would say to put the return part of the render function inside of the .then((value) => { return ( ... ); });. But this is an even worse idea. Basically you need the right lifecycle method here and it's NOT the render method.
Anyway, since I have never used this component library before I can only help nudge you in the right direction so here goes... These docs on their webpage seem to say that you need a reference to the props navigator passed down to the component in which you are using it. So if you created the navigator in this class, you would use this.refs.whateverYouNamedTheNavigatorReference.navigate('SomeItemName'). If you are in the class that has been passed this navigator as a prop, you use this.props.passNavigatorPropName.navigate('SomeItemName'). I see you are using variable deconstruction to get the navigate callback but I would caution on doing this, this way because I have seen it cause errors by grabbing an old version of the navigate function or its parent reference by accident and causing a cascading error effect.
Also, if you are going to be using ASyncStorage in a component file (again, would recommend putting this in a component/class where your data is accessed throughout the app...) and you are going to use it to decide the app should navigate forwards/backwards... definitely remove it from the render function and put it in maybe the constructor, componentWillReceiveProps, componentDidReceiveProps or componentWillUpdate lifecycle functions. That way it fires based on an update, a new passed prop obj or one time as the component is built. Anything is better than firing it every single render.
Lastly, I do not know what you have setup for your StackNavigator route stack object but you would need to have the keyword you used "DashboardScreen" in there pointing to an actual component that has been imported properly. The "DashboardScreen" keyword most likely would connect in your StackNavigator object to some component import like so...
import Dashboard from '../Views/DashboardScreenView';
StackNavigator({
DashboardScreen: {
screen: Dashboard,
path: 'dashboard/:main',
navigationOptions: null,
},
});
There is a simple way here: use "replace" (reference link repleace in navigation ,For example, you are at the screen "Login" ,
and you want to move to screen "Home", insert this code in screen "Login"
<TouchableOpacity onPress={() => { this.login() }}>
<Text}>Click me to Login</Text>
</TouchableOpacity>
and method login:
login(){
this.props.navigation.replace('Home')
}
Screen "Login" will be replaced by "Home", in Android, press Back Button =>app exit, no back screen "Login"
Based on your requirement, i suggest following setup:
SplashNavigator.js
const SplashNavigator = StackNavigator({
Splash: {
screen: SplashScreen,
navigationOptions: {
header: {
visible: false
}
}
}
});
AppNavigator.js
const AppNavigator = StackNavigator(
{
EnableNotification: {
screen: EnableNotificationScreen,
navigationOptions: {
header: {
visible: false
}
}
},
CreateMessage: {
screen: CreateMessageScreen,
navigationOptions: {
header: {
visible: false
}
}
},
Dashboard: {
screen: DashboardScreen,
navigationOptions: {
header: {
visible: false
}
}
}
},
{
initialRouteName: "EnableNotification"
}
);
In your index.android.js, you will render the SplashNavigator.
The SplashNavigator will render the SplashScreen. It has initial state value isReady set to false, so it will render a loading text until the #ProductTour:key value from AsyncStorage is loaded (AsyncStorage is async function, u should not put it in your render function). It will then render your AppNavigator and render your EnableNotification as initial route.
class SplashScreen extends Component {
constructor() {
super(props);
this.state = {
isReady: false,
}
}
componentDidMount() {
AsyncStorage.getItem("#ProductTour:key").then(value => {
console.log(value);
// you will need to handle case when `#ProductTour:key` is not exists
this.setState({
isReady: true,
});
});
}
render() {
const { isReady } = this.state;
return (
<View style={{flex: 1}}>
{
isReady ?
<AppNavigator />
: <Text>Loading</Text>
}
</View>
);
}
}
Then on EnableNotificationScreen and CreateMessageScreen, change your navigate route function to use NavigationActions.reset from doc
Example:
import { NavigationActions } from 'react-navigation';
handleOnPressButton = () => {
const resetAction = NavigationActions.reset({
index: 0,
actions: [
NavigationActions.navigate({ routeName: "CreateMessage" })
]
});
this.props.navigation.dispatch(resetAction);
}
Yes in react native you can finish the current screen before navigating to new screen with the help of NavigationActions . Please refer this link -
http://androidseekho.com/others/reactnative/finish-current-screen-on-navigating-another-in-react-native/
SplashNavigator.js
const SplashNavigator = StackNavigator({
Splash: {
screen: SplashScreen,
navigationOptions: {
header: null}
}
}
});
Import StackActions and NavigationActions from react-navigation.
import { StackActions, NavigationActions } from 'react-navigation';
below code for performing Action
navigateToHomeScreen = () => {
const navigateAction = StackActions.reset({
index: 0,
actions: [NavigationActions.navigate({ routeName: "HomeScreen" })],
});
this.props.navigation.dispatch(navigateAction);
}

Linking event handler not fired when app is in background

I am implementing logging in using Facebook, Twitter, etc.. When the user clicks on the icon, safari opens, the user logs in, and then the webpage redirects to appname://login.
I followed this article https://medium.com/react-native-training/deep-linking-your-react-native-app-d87c39a1ad5e, and my code looks like the following:
componentDidMount(){
Linking.getInitialURL()
.then((url) => {
if (url) {
// Alert.alert('GET INIT URL','initial url ' + url)
this.resetStackToProperRoute(url)
}
})
.catch((e) => {})
Linking.addEventListener('url', this.handleOpenURL);
}
componentWillUnmount() {
Linking.removeEventListener('url', this.handleOpenURL);
}
handleOpenURL = (event) => {
console.log('event: ' + event);
}
The problem is nothing gets printed on the console. I think the problem is that the handler is not fired when the app comes from the background. Is there a way around it?
thanks

JSON value of type NSnull cannot be converted to NSString

I have a strange error on my react-native Firebase APL.
react-native run-android works fine without error.
but react-native run-ios failed with JSON value of type NSnull cannot be converted to NSString.
source code is as follows.(main and signup class to authentication work on firebase)
I think Firebase Class has different ACT on ios and Android to convert JSON to text.
Any suggestion appreciated.
Shoji
main
// Initialize the firebase app here and pass it to other components as needed. Only initialize on startup.
const firebaseApp = firebase.initializeApp(firebaseConfig);
var GiftedMessenger = require('react-native-gifted-messenger');
let styles = {}
class Pricing extends Component {
constructor(props){
super(props);
this.state = {
page: null
};
/* this.itemsRef = this.getRef().child('items'); */
}
componentWillMount(){
// We must asynchronously get the auth state, if we use currentUser here, it'll be null
const unsubscribe = firebaseApp.auth().onAuthStateChanged((user) => {
// If the user is logged in take them to the accounts screen
if (user != null) {
this.setState({page: Account});
console.log('(user != null)')
return;
}
// otherwise have them login
console.log('(user != Login)')
this.setState({page: Login});
// unsubscribe this observer
unsubscribe();
});
}
render() {
if (this.state.page) {
return (
// Take the user to whatever page we set the state to.
// We will use a transition where the new page will slide in from the right.
<Navigator
initialRoute={{component: this.state.page}}
configureScene={() => {
return Navigator.SceneConfigs.FloatFromRight;
}}
renderScene={(route, navigator) => {
if(route.component){
// Pass the navigator the the page so it can navigate as well.
// Pass firebaseApp so it can make calls to firebase.
return React.createElement(route.component, { navigator, firebaseApp});
}
}} />
);
} else {
return (
// Our default loading view while waiting to hear back from firebase
I deeply debug in this BUG.
At last I found it as follows.
<Image
source={{uri: 'this.state.user.photoURL'}} <<<single quotation was added.
style={styles.image}
/>
I did not expect like this kind of error.
Thanks Shoji
took help from #itagaki. Error was removed after replacing single quote with double quote in following code
const BoldText = (props) => <Text style={{fontWeight: 'bold'}}>{props.children}</Text>;

Categories

Resources