add spotify authentication as a button to the app

This commit is contained in:
StarAppeal
2024-11-24 15:39:08 +01:00
parent 861d624216
commit 9697b04ce3
9 changed files with 302 additions and 124 deletions
+1 -1
View File
@@ -5,7 +5,7 @@
"version": "1.0.0",
"orientation": "portrait",
"icon": "./assets/images/icon.png",
"scheme": "myapp",
"scheme": "led.matrix",
"userInterfaceStyle": "automatic",
"newArchEnabled": true,
"ios": {
+31 -15
View File
@@ -1,21 +1,37 @@
import { Text, View, StyleSheet } from 'react-native';
import {StyleSheet, Text, View} from 'react-native';
import React, {useState} from "react";
import SpotifyAuthButton from "@/components/SpotifyAuthButton";
import {Token} from "@/services/RestService";
export default function AboutScreen() {
return (
<View style={styles.container}>
<Text style={styles.text}>About screen</Text>
</View>
);
const [token, setToken] = useState<Token | null>(null);
const handleAuthSuccess = (token: Token) => {
setToken(token);
console.log('Erhaltener Authentifizierungscode:', token);
// Hier kannst du den Code weiterverwenden, um das Access Token zu erhalten
};
return (
<View style={styles.container}>
<Text style={styles.text}>About screen</Text>
<Text>Willkommen bei der Spotify Authentifizierung</Text>
<SpotifyAuthButton onAuthSuccess={handleAuthSuccess}/>
{token && <Text>Erhaltener Code: {token.access_token}</Text>}
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#25292e',
justifyContent: 'center',
alignItems: 'center',
},
text: {
color: '#fff',
},
container: {
flex: 1,
backgroundColor: '#25292e',
justifyContent: 'center',
alignItems: 'center',
},
text: {
color: '#fff',
},
});
+49 -46
View File
@@ -1,62 +1,65 @@
import {ActivityIndicator, StyleSheet, Text, View} from 'react-native';
import Button from '@/components/Button';
import ImageViewer from '@/components/ImageViewer';
const PlaceholderImage = require('@/assets/images/GarfieldCharakter.webp');
import * as ImagePicker from 'expo-image-picker';
import {RestService} from '@/services/RestService';
import useService from '@/hooks/useService';
const PlaceholderImage = require('@/assets/images/GarfieldCharakter.webp');
import * as WebBrowser from "expo-web-browser";
WebBrowser.maybeCompleteAuthSession();
export default function Index() {
const {data, loading, error} = useService(RestService.fetchAllUser);
const {data: usersData, loading: userLoading, error: userError} = useService(RestService.fetchAllUser);
const pickImageAsync = async () => {
let result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ['images'],
allowsEditing: true,
quality: 1,
});
const pickImageAsync = async () => {
let result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ['images'],
allowsEditing: true,
quality: 1,
});
if (!result.canceled) {
console.log(result);
} else {
alert('You did not select any image.');
}
};
if (!result.canceled) {
console.log(result);
} else {
alert('You did not select any image.');
}
};
console.log(data);
return (
<View style={styles.container}>
<View style={styles.imageContainer}>
<ImageViewer imgSource={PlaceholderImage}/>
console.log(usersData);
return (
<View style={styles.container}>
<View style={styles.imageContainer}>
<ImageViewer imgSource={PlaceholderImage}/>
</View>
<View style={styles.footerContainer}>
{userLoading && <ActivityIndicator/>}
{userError && <Text>Error: {userError.message}</Text>}
{usersData && (
<Text>
{usersData.users.map((item) => item.name).join('; ')}
</Text>
)}
<Button label="Choose a photo" theme="primary" onPress={pickImageAsync}/>
<Button label="Use this photo"/>
</View>
</View>
<View style={styles.footerContainer}>
{loading && <ActivityIndicator/>}
{error && <Text>Error: {error.message}</Text>}
{data && (
<Text>
{data.users.map((item) => item.name).join('; ')}
</Text>
)}
<Button label="Choose a photo" theme="primary" onPress={pickImageAsync}/>
<Button label="Use this photo"/>
</View>
</View>
);
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#25292e',
alignItems: 'center',
},
imageContainer: {
flex: 1,
},
footerContainer: {
flex: 1 / 3,
alignItems: 'center',
},
container: {
flex: 1,
backgroundColor: '#25292e',
alignItems: 'center',
},
imageContainer: {
flex: 1,
},
footerContainer: {
flex: 1 / 3,
alignItems: 'center',
},
});
+26
View File
@@ -0,0 +1,26 @@
import React from 'react';
import {Button} from 'react-native';
import {useSpotifyAuth} from '@/hooks/useSpotifyAuth';
import {Token} from "@/services/RestService";
interface SpotifyAuthButtonProps {
onAuthSuccess: (token: Token) => void;
}
const SpotifyAuthButton = ({onAuthSuccess}: SpotifyAuthButtonProps) => {
const {promptAuth, isReady, error} = useSpotifyAuth(onAuthSuccess);
if (error) {
console.error('Spotify Auth Error:', error);
}
return (
<Button
disabled={!isReady}
title={'Login mit Spotify'}
onPress={promptAuth}
/>
);
};
export default SpotifyAuthButton;
+2 -3
View File
@@ -2,7 +2,6 @@ import {useState, useEffect} from 'react';
type AsyncCallback<T> = () => Promise<T>;
interface ServiceResult<T> {
data: T | null,
error: Error | null,
@@ -18,7 +17,7 @@ const useService = <T>(callback: AsyncCallback<T>): ServiceResult<T> => {
const executeCallback = async () => {
setLoading(true);
try {
const result = await callback(); // Führe den übergebenen Callback aus
const result = await callback();
setData(result);
} catch (err) {
setError(err as Error);
@@ -33,4 +32,4 @@ const useService = <T>(callback: AsyncCallback<T>): ServiceResult<T> => {
return {data, loading, error};
}
export default useService;
export default useService;
+54
View File
@@ -0,0 +1,54 @@
import {useEffect} from "react";
import {makeRedirectUri, useAuthRequest} from "expo-auth-session";
import {RestService, Token} from "@/services/RestService";
const discovery = {
authorizationEndpoint: 'https://accounts.spotify.com/authorize',
tokenEndpoint: 'https://accounts.spotify.com/api/token',
};
interface UseSpotifyAuthResult {
promptAuth: () => void;
isReady: boolean;
error: Error | null;
}
export const useSpotifyAuth = (
onAuthSuccess: (token: Token) => void
): UseSpotifyAuthResult => {
const [request, response, promptAsync] = useAuthRequest(
{
clientId: process.env.EXPO_PUBLIC_SPOTIFY_CLIENT_ID!,
scopes: ['user-read-currently-playing'],
usePKCE: false,
redirectUri: makeRedirectUri({
scheme: 'led.matrix',
path: 'callback',
}),
},
discovery
);
useEffect(() => {
const handleAuthResponse = async () => {
if (response?.type === 'success') {
try {
const {code} = response.params;
const token = (await RestService.exchangeSpotifyCodeForToken(code)).token;
console.log('Token:', token);
onAuthSuccess(token);
} catch (error) {
console.error('Fehler bei der Authentifizierung:', error);
}
}
};
handleAuthResponse();
}, [response]);
return {
promptAuth: () => promptAsync(),
isReady: !!request,
error: response?.type === 'error' ? new Error('Auth fehlgeschlagen') : null,
};
};
+59
View File
@@ -14,6 +14,7 @@
"@react-navigation/native": "^7.0.0",
"axios": "^1.7.7",
"expo": "^52.0.10",
"expo-auth-session": "^6.0.0",
"expo-blur": "~14.0.1",
"expo-constants": "~17.0.3",
"expo-dev-client": "~5.0.4",
@@ -7242,6 +7243,15 @@
}
}
},
"node_modules/expo-application": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/expo-application/-/expo-application-6.0.1.tgz",
"integrity": "sha512-w+1quSmKp8SYKT+GAFHSN5c6u+PqoVRIfpsLyRQrQdOnBA9dA8Hw6JT9sHNFmA30A2v1b/sdYZE3qKuRJFNSWQ==",
"license": "MIT",
"peerDependencies": {
"expo": "*"
}
},
"node_modules/expo-asset": {
"version": "11.0.1",
"resolved": "https://registry.npmjs.org/expo-asset/-/expo-asset-11.0.1.tgz",
@@ -7259,6 +7269,24 @@
"react-native": "*"
}
},
"node_modules/expo-auth-session": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/expo-auth-session/-/expo-auth-session-6.0.0.tgz",
"integrity": "sha512-t40IvmUnWPdSFTr/d3FxDo3qbHdt6hPoRApZ9KH8/UoTjkdoSKnxi6W0/svpISDPMi25gB0lNYwy72YUisl1Yw==",
"license": "MIT",
"dependencies": {
"expo-application": "~6.0.0",
"expo-constants": "~17.0.0",
"expo-crypto": "~14.0.0",
"expo-linking": "~7.0.0",
"expo-web-browser": "~14.0.0",
"invariant": "^2.2.4"
},
"peerDependencies": {
"react": "*",
"react-native": "*"
}
},
"node_modules/expo-blur": {
"version": "14.0.1",
"resolved": "https://registry.npmjs.org/expo-blur/-/expo-blur-14.0.1.tgz",
@@ -7284,6 +7312,18 @@
"react-native": "*"
}
},
"node_modules/expo-crypto": {
"version": "14.0.1",
"resolved": "https://registry.npmjs.org/expo-crypto/-/expo-crypto-14.0.1.tgz",
"integrity": "sha512-/gGpD9UAz8fgZtU08cwwqeQElkFmMy2Hc8lLa9laSjD3YN0XM07zDJyJ+CC1VhQ63G8WpUnq1IHSmaPbbLp+oQ==",
"license": "MIT",
"dependencies": {
"base64-js": "^1.3.0"
},
"peerDependencies": {
"expo": "*"
}
},
"node_modules/expo-dev-client": {
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/expo-dev-client/-/expo-dev-client-5.0.4.tgz",
@@ -12634,6 +12674,25 @@
}
}
},
"node_modules/react-native-app-auth": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/react-native-app-auth/-/react-native-app-auth-8.0.0.tgz",
"integrity": "sha512-uAlbR8ggVmFvH1zQwBWzksRTwpBMVIhAKsLhidwwlUH5idyTN4b4hJO7oDIa5FbvAAYcEZU5d0ChZDoIklomFw==",
"license": "MIT",
"dependencies": {
"invariant": "2.2.4",
"react-native-base64": "0.0.2"
},
"peerDependencies": {
"react-native": ">=0.63.0"
}
},
"node_modules/react-native-base64": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/react-native-base64/-/react-native-base64-0.0.2.tgz",
"integrity": "sha512-Fu/J1a2y0X22EJDWqJR2oEa1fpP4gTFjYxk8ElJdt1Yak3HOXmFJ7EohLVHU2DaQkgmKfw8qb7u/48gpzveRbg==",
"license": "MIT"
},
"node_modules/react-native-gesture-handler": {
"version": "2.20.2",
"resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.20.2.tgz",
+1
View File
@@ -21,6 +21,7 @@
"@react-navigation/native": "^7.0.0",
"axios": "^1.7.7",
"expo": "^52.0.10",
"expo-auth-session": "^6.0.0",
"expo-blur": "~14.0.1",
"expo-constants": "~17.0.3",
"expo-dev-client": "~5.0.4",
+79 -59
View File
@@ -1,72 +1,92 @@
import axios from 'axios';
import {makeRedirectUri} from "expo-auth-session";
import User from "@/model/User";
const API_URL = process.env.EXPO_PUBLIC_API_URL;
const JWT_TOKEN = process.env.EXPO_PUBLIC_JWT_TOKEN;
export interface Token {
access_token: string,
refresh_token: string,
expires_in: number,
token_type: string,
scope: string,
}
const RestService = {
fetchAllUser: async () => {
try {
const response = await axios.get<{ users: User[] }>(`${API_URL}/user`, {
headers: {
Authorization: `Bearer ${JWT_TOKEN}`,
},
});
return response.data;
} catch (error) {
throw error;
}
},
exchangeSpotifyCodeForToken: async (code: string) => {
try {
const redirectUri = makeRedirectUri({
scheme: 'led.matrix',
path: 'callback',
});
const response = await axios.get<{ token: Token }>(
`${API_URL}/spotify/token/generate/code/${code}/redirect-uri/${encodeURIComponent(redirectUri)}`, {
headers: {
Authorization: `Bearer ${JWT_TOKEN}`,
}
}
);
return response.data;
} catch (error) {
console.error("Error exchanging Spotify code:", error);
throw error;
}
},
getCurrentlyPlayingSong: async (accessToken: string) => {
try {
const response = await axios.get('https://api.spotify.com/v1/me/player/currently-playing', {
headers: {
Authorization: `Bearer ${accessToken}`,
},
});
return response.data;
} catch (error) {
throw error;
}
},
fetchAllUser: async () => {
try {
const response = await axios.get<{ users: User[] }>(`${API_URL}/user`, {
headers: {
Authorization: `Bearer ${JWT_TOKEN}`,
},
});
return response.data;
} catch (error) {
console.error("Error fetching users:", error);
throw error;
}
},
sendPayloadToSocket: async (userId: string, payload: Object) => {
try {
const response = await axios.post(
`${API_URL}/websocket/send-message`,
{users: [userId], payload},
{
headers: {
Authorization: `Bearer ${JWT_TOKEN}`,
'Content-Type': 'application/json',
},
}
);
return response.data;
} catch (error) {
throw error;
}
},
sendPayloadToSocket: async (userId: string, payload: object) => {
try {
const response = await axios.post(
`${API_URL}/websocket/send-message`,
{users: [userId], payload},
{
headers: {
Authorization: `Bearer ${JWT_TOKEN}`,
'Content-Type': 'application/json',
},
}
);
return response.data;
} catch (error) {
console.error("Error sending payload to socket:", error);
throw error;
}
},
broadcast: async (payload: Object) => {
try {
const response = await axios.post(
`${API_URL}/websocket/broadcast`,
{payload},
{
headers: {
Authorization: `Bearer ${JWT_TOKEN}`,
'Content-Type': 'application/json',
},
}
);
return response.data;
} catch (error) {
throw error;
}
},
broadcast: async (payload: object) => {
try {
const response = await axios.post(
`${API_URL}/websocket/broadcast`,
{payload},
{
headers: {
Authorization: `Bearer ${JWT_TOKEN}`,
'Content-Type': 'application/json',
},
}
);
return response.data;
} catch (error) {
console.error("Error broadcasting payload:", error);
throw error;
}
},
};
export {RestService};