diff --git a/app.json b/app.json
index 0aa210f..1dc49c4 100644
--- a/app.json
+++ b/app.json
@@ -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": {
diff --git a/app/(tabs)/about.tsx b/app/(tabs)/about.tsx
index 105598f..294ef22 100644
--- a/app/(tabs)/about.tsx
+++ b/app/(tabs)/about.tsx
@@ -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 (
-
- About screen
-
- );
+ const [token, setToken] = useState(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 (
+
+ About screen
+ Willkommen bei der Spotify Authentifizierung
+
+ {token && Erhaltener Code: {token.access_token}}
+
+
+
+ );
}
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',
+ },
});
diff --git a/app/(tabs)/index.tsx b/app/(tabs)/index.tsx
index bf2965e..0a6bc44 100644
--- a/app/(tabs)/index.tsx
+++ b/app/(tabs)/index.tsx
@@ -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 (
-
-
-
+ console.log(usersData);
+ return (
+
+
+
+
+
+ {userLoading && }
+ {userError && Error: {userError.message}}
+ {usersData && (
+
+ {usersData.users.map((item) => item.name).join('; ')}
+
+ )}
+
-
- {loading && }
- {error && Error: {error.message}}
- {data && (
-
- {data.users.map((item) => item.name).join('; ')}
-
- )}
-
-
-
-
- );
+ );
}
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',
+ },
});
diff --git a/components/SpotifyAuthButton.tsx b/components/SpotifyAuthButton.tsx
new file mode 100644
index 0000000..21544f1
--- /dev/null
+++ b/components/SpotifyAuthButton.tsx
@@ -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 (
+
+ );
+};
+
+export default SpotifyAuthButton;
diff --git a/hooks/useService.ts b/hooks/useService.ts
index d36afbb..fe79010 100644
--- a/hooks/useService.ts
+++ b/hooks/useService.ts
@@ -2,7 +2,6 @@ import {useState, useEffect} from 'react';
type AsyncCallback = () => Promise;
-
interface ServiceResult {
data: T | null,
error: Error | null,
@@ -18,7 +17,7 @@ const useService = (callback: AsyncCallback): ServiceResult => {
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 = (callback: AsyncCallback): ServiceResult => {
return {data, loading, error};
}
-export default useService;
\ No newline at end of file
+export default useService;
diff --git a/hooks/useSpotifyAuth.ts b/hooks/useSpotifyAuth.ts
new file mode 100644
index 0000000..7a8694e
--- /dev/null
+++ b/hooks/useSpotifyAuth.ts
@@ -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,
+ };
+};
diff --git a/package-lock.json b/package-lock.json
index d88e20f..c63ce6a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -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",
diff --git a/package.json b/package.json
index cda3687..c56b26c 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/services/RestService.ts b/services/RestService.ts
index 4a3589b..e29891e 100644
--- a/services/RestService.ts
+++ b/services/RestService.ts
@@ -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};