How can I authenticate Spotify in a React Native component? - react-native

I am creating a React Native mobile application for a school project and I want a user to be able to login to Spotify to be able to get information about their playback using the Spotify API.
I am developing for iOS and using Expo, so have found this documentation from Expo to be quite helpful. Using their sample Auth Code, I was successfully able to make a very simple React app that allows the user to push a button that prompts them to login with Spotify. For my project, though, I have different components corresponding to different screens in my app. Whenever I try to move the code into a component, I get an error that I am unsure how to resolve (I am still pretty new to React and Javascript).
Here is the code:
import * as React from 'react';
import * as WebBrowser from 'expo-web-browser';
import { makeRedirectUri, useAuthRequest } from 'expo-auth-session';
import { View, Button, StyleSheet } from 'react-native';
import { LoginContext } from '../LoginContext';
WebBrowser.maybeCompleteAuthSession();
// Endpoint
const discovery = {
authorizationEndpoint: 'https://accounts.spotify.com/authorize',
tokenEndpoint: 'https://accounts.spotify.com/api/token',
};
const [request, response, promptAsync] = useAuthRequest(
{
clientId: CLIENT_ID,
scopes: ['user-read-email', 'playlist-modify-public'],
// In order to follow the "Authorization Code Flow" to fetch token after authorizationEndpoint
// this must be set to false
usePKCE: false,
redirectUri: 'exp://localhost:19000/--/',
},
discovery
);
React.useEffect(() => {
if (response?.type === 'success') {
const { code } = response.params;
}
}, [response]);
export default class Settings extends React.Component {
render () {
return (
<View style={{flex: 1, display: 'flex', justifyContent: 'center', alignItems: "center", alignContent: 'center'}}>
<Button
//disabled={!request}
title="Login to Spotify"
onPress={() => {
//console.log("Login attempt");
promptAsync();
}}
/>
</View>
);
}
}
I get an error message that states:
Error: Invalid hook call. Hooks can only be called inside of the body
of a function component. This could happen for one of the following
reasons:
You might have mismatching versions of React and the renderer (such as React DOM)
You might be breaking the Rules of Hooks
You might have more than one copy of React in the same app See https://reactjs.org/warnings/invalid-hook-call-warning.html for tips about how to debug and
fix this problem.
I looked at the link in the error message to see if I could debug myself, but I am having some trouble given that I am still pretty new to React. Any help would be greatly appreciated!

useAuthRequest and useEffect are what we call React hooks. They can be used in functional components only. In my opinion, you should first check the documentation of how to use hooks in React: https://reactjs.org/docs/hooks-intro.html
WebBrowser.maybeCompleteAuthSession();
// Endpoint
const discovery = {
authorizationEndpoint: 'https://accounts.spotify.com/authorize',
tokenEndpoint: 'https://accounts.spotify.com/api/token',
};
// This is the functional component
const Settings = () => {
const [request, response, promptAsync] = useAuthRequest({
clientId: CLIENT_ID,
scopes: ['user-read-email', 'playlist-modify-public'],
// In order to follow the "Authorization Code Flow" to fetch token after authorizationEndpoint
// this must be set to false
usePKCE: false,
redirectUri: 'exp://localhost:19000/--/',
}, discovery);
React.useEffect(() => {
if (response?.type === 'success') {
const { code } = response.params;
}
}, [response]);
return (
<View>...</View>
)
}

Related

How to handle Auth with WebBrowser.openAuthSessionAsync in Expo?

I created a auth flow using WebBrowser.openAuthSessionAsync, the opening and the closing are working as expected but my problem comes with the return, I only receive back from the browser: {"type": "success", "url": "exp://192.168.0.11:19000/--/App"}, the type and my return URL.
I'm trying to figure out how to have access to any kind of info from the return, from cookies to query string on the URL as I have control over how my API handles the return, but I'm not being able to access any kind of info from the AuthSession.
Here is the implementation:
import { useEffect, useState } from "react"
import { Button, View, Text } from "react-native"
import * as WebBrowser from "expo-web-browser"
import * as Linking from "expo-linking"
export default () => {
const [result, setResult] =
useState<WebBrowser.WebBrowserAuthSessionResult | null>(null)
useEffect(() => {
console.log(result)
}, [result])
const _handlePressButtonAsync = async () => {
const baseUrl = "https://...com"
const callbackUrl = Linking.createURL("App", { scheme: "myapp" })
setResult(
await WebBrowser.openAuthSessionAsync(
`${baseUrl}/login?returnUrl=${encodeURIComponent(
`${baseUrl}/_v/login?token=...&iv=...&returnUrl=${callbackUrl}`
)}`,
callbackUrl
)
)
}
return (
<View className="items-center justify-center flex-1">
<Button title="Open Auth Session" onPress={_handlePressButtonAsync} />
{result && <Text>{JSON.stringify(result)}</Text>}
</View>
)
}
It's a SASS platform, that does not follow only the OAuth flow, which made me choose this method over https://docs.expo.dev/versions/latest/sdk/auth-session/

unable to display expo push token on apk

I am using react native expo bare workflow
When I open the app on expo client I am able to see the device token on screen but when I convert it to apk
by using
expo build:android
It doesn't show token.
This is my code.
import React, { useEffect, useState } from "react";
import { StyleSheet, Text, View } from "react-native";
import * as Notifications from "expo-notifications";
import * as Permissions from "expo-permissions";
import { TextInput } from "react-native";
export default function App() {
const [token, setToken] = useState("");
const [value, onChangeText] = React.useState("Useless Placeholder");
useEffect(() => {
getPushNotificationPermissions();
});
getPushNotificationPermissions = async () => {
const { status: existingStatus } = await Permissions.getAsync(
Permissions.NOTIFICATIONS,
);
let finalStatus = existingStatus;
// only ask if permissions have not already been determined, because
// iOS won't necessarily prompt the user a second time.
if (existingStatus !== "granted") {
// Android remote notification permissions are granted during the app
// install, so this will only ask on iOS
const { status } = await Permissions.askAsync(Permissions.NOTIFICATIONS);
finalStatus = status;
}
// Stop here if the user did not grant permissions
if (finalStatus !== "granted") {
return;
}
console.log(finalStatus);
// Get the token that uniquely identifies this device
console.log(
"Notification Token: ",
(await Notifications.getExpoPushTokenAsync()).data,
);
setToken((await Notifications.getExpoPushTokenAsync()).data);
};
return (
<View style={styles.container}>
<Text>{token}</Text>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#fff",
alignItems: "center",
justifyContent: "center",
},
});
And also i don't get a push notification on apk but it works on expo client.
Since you're using bare workflow, You should pass your experienceId in an object to getExpoPushTokenAsync, the value of your experienceId is #your-username/your-project-slug

AWS amplify remember logged in user in React Native app

I just started exploring AWS amplify as a backend for my react native application. Being a true beginner on using the service, I want my app to remember the logged in user every time I refresh the emulator.
I know from AWS amplify documentation that I can use the Auth function currentAuthenticatedUser for this purpose, but I have no idea on how to implement a code that does this purpose.
My app looks like this:
App.js
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
import AuthTabs from './components/AuthTabs';
import NavigationTab from './components/NavigationTab';
import Amplify, { Auth } from 'aws-amplify';
import AWSConfig from './aws-exports';
Amplify.configure(AWSConfig);
export default class App extends React.Component {
state = {
isAuthenticated: false
}
authenticate(isAuthenticated) {
this.setState({ isAuthenticated })
}
render() {
if (this.state.isAuthenticated) {
console.log('Hello', Auth.user.username)
return(
<View style={styles.container}>
<Text style={styles.textStyle}>
Hello {Auth.user.username}!
</Text>
<NavigationTab
screenProps={
{authenticate: this.authenticate.bind(this)}
}
/>
</View>
)
}
return (
<View style={styles.container}>
<AuthTabs
screenProps={
{authenticate: this.authenticate.bind(this)}
}
/>
</View>
)
}
}
Any help would be much appreciated.
i have used it like this:
currentUser = () => {
Auth.currentAuthenticatedUser()
.then(user => {
console.log("USER", user);
this.props.navigation.navigate("App");
})
.catch(err => {
console.log("ERROR", err);
});
};
so, one can call it on the constructor on app refresh, and if the user is authenticated go to the main screen, but if it's not, stay in the login screen. Cheers.
I also have come up with a similar solution. But instead of the constructor, I use the life cycle method componentDidMount() to call a method that I named loadApp().
import React from 'react'
import {
StyleSheet,
View,
ActivityIndicator,
} from 'react-native'
import Auth from '#aws-amplify/auth'
export default class AuthLoadingScreen extends React.Component {
state = {
userToken: null
}
async componentDidMount () {
await this.loadApp()
}
// Get the logged in users and remember them
loadApp = async () => {
await Auth.currentAuthenticatedUser()
.then(user => {
this.setState({userToken: user.signInUserSession.accessToken.jwtToken})
})
.catch(err => console.log(err))
this.props.navigation.navigate(this.state.userToken ? 'App' : 'Auth')
}
render() {
return (
<View style={styles.container}>
<ActivityIndicator size="large" color="#fff" />
</View>
)
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#aa73b7',
alignItems: 'center',
justifyContent: 'center',
},
})
loadApp() will try and get the user JWT Token by calling the AWS Amplify currentAuthenticatedUser() method. The obtained token is then stored in the component state.
I have used React navigation version 2 to navigate the user to either the App screen or the Auth stack screen depending on her status: logged in or not logged in.
Here is the way I handled this issue:
const currentUserInfo = await Auth.currentUserInfo()
if (currentUserInfo){
const data = await Auth.currentAuthenticatedUser()
dispatch({authTypes.FETCH_USER_DATA_SUCCESS, {payload: {user: data}}});
}

How to implment role based authorizarion with React Navigation?

I know this is not an usual requirement, but is it possible to create a role based authorization system using React Navigation? If yes, is there a complementary tool to achieve that? Or can this be made using only React Navigation?
There are a lot of ways to do authorization rules using react navigation library.
Here are some good articles to follow:
https://medium.com/the-react-native-log/building-an-authentication-flow-with-react-navigation-fb5de2203b5c
https://reactnavigation.org/docs/auth-flow.html
As I use redux-saga, I like to use it to control authentication flow, because it's easy to handle in a more linear way, listening to redux-persist actions
I think does not exists a right way to do this, because it depends a lot according with yours needs, application flow and backend.
If you integrate react-navigation with redux, you will be able to intercept all the navigation actions (with navigate/ prefix. Eg: navigate/HOME) in a redux middleware. You can write your own logic in the middleware to only let authorized actions to reach the reducer.
Follow this guide for integrating react-navigation to redux - https://reactnavigation.org/docs/redux-integration.html.
This video will help you with using middleware for this purpose - https://www.youtube.com/watch?v=Gjiu7Lgdg3s.
This is simple logic.
Here is my logic which worked perfectly...
RequireAuth.js
import React, { Component } from "react";
import { authedUser } from '../../../helper/helpers';
import { Lang } from '../../../helper/Lang';
import Login from "../../screens/form/login";
import {
Container,
Header,
Title,
Content,
Button,
Icon,
H1,
H2,
H3,
Text,
Left,
Right,
Body
} from "native-base";
import styles from "./styles";
const RequireAuth =(obj)=>{
const Component=obj.component;
return class App extends Component {
state = {
isAuthenticated: false,
isLoading: true
}
componentDidMount() {
authedUser({loaduserow:false,noCatch:true}).then((res) => {
if(res.loged){
this.setState({isAuthenticated: true, isLoading: false});}
else{
this.setState({isAuthenticated: false, isLoading: false});}
}).catch(() => {
this.setState({isLoading: false});
})
}
render() {
const { isAuthenticated, isLoading } = this.state;
if(isLoading) {
return(
<Container style={styles.container}>
<Content padder>
<H3 style={{marginTop:20,marginBottom:30, borderBottomWidth: 1}}>Authenticating...</H3>
</Content>
</Container>)
}
if(!isAuthenticated) {
return <Login {...this.props} />
}
return <Component {...this.props} />
}
}}
export default RequireAuth;
src/App.js
....
import RequireAuth from "./screens/wall/RequireAuth";
...
const AppNavigator = StackNavigator(
{
Drawer: { screen: Drawer },
Login: { screen: Login },
About: { screen: About },
Profile: { screen: RequireAuth({component:Profile,name:'Profile'}) },
.....
authedUser() is just a simple promise which return resolve({loged:true}) if authed while not auths it returns resolve({loged:false})
Check this below this might help you
https://jasonwatmore.com/post/2019/02/01/react-role-based-authorization-tutorial-with-example

Expo.FileSystem.downloadAsync do not show download notification

I am using expo FileSystem to download the pdf file. The API response lands into success function. However, I am not able to show the downloaded file to the user.
The expected behaviour should be like we usually see notification icon on the status bar and on click on icon its opens your file.
FileSystem.downloadAsync(
'https://bitcoin.org/bitcoin.pdf',
FileSystem.documentDirectory + 'Stay_Overview.xlsx'
).then(({ uri }) => {
console.log('Finished downloading to ', uri);
})
.catch(error => {
console.error(error);
});
This one had one or two tricks, but here is a solution to this using Expo that works on both iOS and Android.
In a new Expo project, amend the following two files:
App.js
import React, { Component } from 'react';
import { View, ScrollView, StyleSheet, Button, Alert, Platform, Text, TouchableWithoutFeedback } from 'react-native';
import { FileSystem, Constants, Notifications, Permissions } from 'expo';
import Toast, {DURATION} from 'react-native-easy-toast';
async function getiOSNotificationPermission() {
const { status } = await Permissions.getAsync(
Permissions.NOTIFICATIONS
);
if (status !== 'granted') {
await Permissions.askAsync(Permissions.NOTIFICATIONS);
}
}
export default class App extends Component {
constructor(props) {
super(props);
// this.toast = null;
this.listenForNotifications = this.listenForNotifications.bind(this);
// this.openFile = this.openFile.bind(this);
this.state = {
filePreviewText: ''
}
}
_handleButtonPress = () => {
let fileName = 'document.txt';
let fileUri = FileSystem.documentDirectory + fileName;
FileSystem.downloadAsync(
"https://raw.githubusercontent.com/expo/expo/master/README.md",
fileUri
).then(({ uri }) => {
console.log('Finished downloading to ', uri);
const localnotification = {
title: 'Download has finished',
body: fileName + " has been downloaded. Tap to open file.",
android: {
sound: true,
},
ios: {
sound: true,
},
data: {
fileUri: uri
},
};
localnotification.data.title = localnotification.title;
localnotification.data.body = localnotification.body;
let sendAfterFiveSeconds = Date.now();
sendAfterFiveSeconds += 3000;
const schedulingOptions = { time: sendAfterFiveSeconds };
Notifications.scheduleLocalNotificationAsync(
localnotification,
schedulingOptions
);
})
.catch(error => {
console.error(error);
Alert.alert(error);
});
};
listenForNotifications = () => {
const _this = this;
Notifications.addListener(notification => {
if (notification.origin === 'received') {
// We could also make our own design for the toast
// _this.refs.toast.show(<View><Text>hello world!</Text></View>);
const toastDOM =
<TouchableWithoutFeedback
onPress={() => {this.openFile(notification.data.fileUri)}}
style={{padding: '10', backgroundColor: 'green'}}>
<Text style={styles.toastText}>{notification.data.body}</Text>
</TouchableWithoutFeedback>;
_this.toast.show(toastDOM, DURATION.FOREVER);
} else if (notification.origin === 'selected') {
this.openFile(notification.data.fileUri);
}
// Expo.Notifications.setBadgeNumberAsync(number);
// Notifications.setBadgeNumberAsync(10);
// Notifications.presentLocalNotificationAsync(notification);
// Alert.alert(notification.title, notification.body);
});
};
componentWillMount() {
getiOSNotificationPermission();
this.listenForNotifications();
}
componentDidMount() {
// let asset = Asset.fromModule(md);
// Toast.show('Hello World');
}
openFile = (fileUri) => {
this.toast.close(40);
console.log('Opening file ' + fileUri);
FileSystem.readAsStringAsync(fileUri)
.then((fileContents) => {
// Get file contents in binary and convert to text
// let fileTextContent = parseInt(fileContents, 2);
this.setState({filePreviewText: fileContents});
});
}
render() {
return (
<View style={styles.container}>
<View style={styles.buttonsContainer}>
<Button style={styles.button}
title={"Download text file"}
onPress={this._handleButtonPress}
/>
<Button style={styles.button}
title={"Clear File Preview"}
onPress={() => {this.setState({filePreviewText: ""})}}
/>
</View>
<ScrollView style={styles.filePreview}>
<Text>{this.state.filePreviewText}</Text>
</ScrollView>
<Toast ref={ (ref) => this.toast=ref }/>
</View>
);
// <Toast
// ref={ (ref) => this.toast=ref }
// style={{backgroundColor:'green'}}
// textStyle={{color:'white'}}
// position={'bottom'}
// positionValue={100}
// opacity={0.8}
// />
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
paddingTop: Constants.statusBarHeight,
backgroundColor: '#ecf0f1',
},
buttonsContainer: {
flexDirection: 'row',
},
button: {
flex: 1
},
filePreview: {
flex: 1,
padding: 10,
},
toastText: {
color: 'white',
padding: 5,
justifyContent: 'flex-start',
},
});
package.json: Add the following dependency (fork of react-native-easy-toast)
"react-native-easy-toast": "git+https://github.com/SiavasFiroozbakht/react-native-easy-toast.git"
There are a couple of important notes about this solution:
Uses Expo API the most, for external local notifications and writing to / reading from files, which limits the current solution to being unable to write to other locations than Expo's own directory.
Once the file is downloaded, either a customisable toast is shown to the user if the app is active (Expo currently does not support foreground notifications), or sends a local Push Notification to let the user know the download has finished. Clicking on any of these two will show the contents of the file in a View, using the <Text> component.
The crazycodeboy/react-native-easy-toast repo has not been used directly due to a limitation of the toast, which is that the touch events are currently disregarded. The forked repo makes this functionality available before the merge request is implemented in the original. I recommend switching back to the original one once it gets patched as I will likely not maintain mine.
Although this project is also available in Snack, it will not run due to the need of using a git repository in package.json as mentioned above, and other apparent inconsistencies in variable scoping. This would be fixed by either the merge request or the new feature in Snack.
Other file types may be supported, either by Expo itself or via external packages, such as this PDF viewer. However, the code will have to be further adapted.
The toast (internal notification) is created with a TouchableWithoutFeedback component, although there are other similar ones in React Native with various differences. This component can be customised in the code (search for toastDOM), but might even be replaceable in the future by internal notifications available in Expo.
Lastly, an intentional three-second delay is applied to the notification once the file is downloaded – this allows us to test the notification when the app is in background. Feel free to remove the delay and trigger the notification immediately.
And that's it! I think this gives a good starting point for file downloading and previewing with Expo.
Codebase also available on GitHub.
DownloadManager
I believe that you are looking to use the DownloadManager for handling your downloads on Android (be aware there is no DownloadManager for iOS so you would have to handle this differently) The DownloadManager either savse the file to a shared system cache or it would save it to external storage.
However, at this time I do not believe that Expo allows you to use the DownloadManager, instead handling all the downloads itself. The reason could be that Expo doesn't allow you access to external storage, as it states in the documentation:
Each app only has read and write access to locations under the following directories:
Expo.FileSystem.documentDirectory
Expo.FileSystem.cacheDirectory
https://docs.expo.io/versions/latest/sdk/filesystem
So there would be no way to access the file in Expo once it was downloaded.
Possible Solution
A possible solution would be to use React-Native Fetch Blob. It does allow you to use the DownloadManager.
https://github.com/joltup/rn-fetch-blob#android-media-scanner-and-download-manager-support
Using the DownloadManager can be achieved in rn-fetch-blob by setting the addAndroidDownloads with the useDownloadManager key set to true.
However, that would mean ejecting your application from Expo.