@superfan-app/spotify-auth 0.1.54 → 0.1.55
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/SpotifyAuth.types.d.ts +29 -2
- package/build/SpotifyAuth.types.d.ts.map +1 -1
- package/build/SpotifyAuth.types.js +7 -1
- package/build/SpotifyAuth.types.js.map +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +36 -10
- package/build/index.js.map +1 -1
- package/ios/SpotifyAuthAuth.swift +92 -37
- package/ios/SpotifyAuthModule.swift +64 -30
- package/package.json +1 -1
- package/src/SpotifyAuth.types.ts +37 -3
- package/src/index.tsx +38 -9
|
@@ -9,6 +9,10 @@ export type SpotifyScopes = 'app-remote-control' | 'playlist-modify-private' | '
|
|
|
9
9
|
export interface SpotifyAuthEvent {
|
|
10
10
|
success: boolean;
|
|
11
11
|
token: string | null;
|
|
12
|
+
refreshToken: string | null;
|
|
13
|
+
expiresIn: number | null;
|
|
14
|
+
tokenType: string | null;
|
|
15
|
+
scope: string | null;
|
|
12
16
|
error?: SpotifyAuthError;
|
|
13
17
|
}
|
|
14
18
|
/**
|
|
@@ -19,6 +23,14 @@ export interface SpotifyAuthorizationData {
|
|
|
19
23
|
success: boolean;
|
|
20
24
|
/** The access token if authorization was successful, null otherwise */
|
|
21
25
|
token: string | null;
|
|
26
|
+
/** The refresh token if authorization was successful, null otherwise */
|
|
27
|
+
refreshToken: string | null;
|
|
28
|
+
/** The token expiration time in seconds if authorization was successful, null otherwise */
|
|
29
|
+
expiresIn: number | null;
|
|
30
|
+
/** The token type (e.g. "Bearer") if authorization was successful, null otherwise */
|
|
31
|
+
tokenType: string | null;
|
|
32
|
+
/** The granted scopes if authorization was successful, null otherwise */
|
|
33
|
+
scope: string | null;
|
|
22
34
|
/** Error information if authorization failed */
|
|
23
35
|
error?: SpotifyAuthError;
|
|
24
36
|
}
|
|
@@ -69,11 +81,26 @@ export interface SpotifyAuthViewProps {
|
|
|
69
81
|
name: string;
|
|
70
82
|
}
|
|
71
83
|
/**
|
|
72
|
-
*
|
|
84
|
+
* Spotify authentication state containing all token-related information
|
|
73
85
|
*/
|
|
74
|
-
export interface
|
|
86
|
+
export interface SpotifyAuthState {
|
|
75
87
|
/** The current Spotify access token, null if not authenticated */
|
|
76
88
|
accessToken: string | null;
|
|
89
|
+
/** The current refresh token, null if not authenticated */
|
|
90
|
+
refreshToken: string | null;
|
|
91
|
+
/** The token expiration time in seconds, null if not authenticated */
|
|
92
|
+
expiresIn: number | null;
|
|
93
|
+
/** The token type, null if not authenticated */
|
|
94
|
+
tokenType: string | null;
|
|
95
|
+
/** The token scope, null if not authenticated */
|
|
96
|
+
scope: string | null;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Context for Spotify authentication state and actions
|
|
100
|
+
*/
|
|
101
|
+
export interface SpotifyAuthContext {
|
|
102
|
+
/** The complete Spotify authentication state */
|
|
103
|
+
authState: SpotifyAuthState;
|
|
77
104
|
/** Function to initiate Spotify authorization */
|
|
78
105
|
authorize: (config: AuthorizeConfig) => Promise<void>;
|
|
79
106
|
/** Whether authorization is in progress */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SpotifyAuth.types.d.ts","sourceRoot":"","sources":["../src/SpotifyAuth.types.ts"],"names":[],"mappings":"AAEA;;;GAGG;AACH,MAAM,MAAM,aAAa,GACrB,oBAAoB,GACpB,yBAAyB,GACzB,wBAAwB,GACxB,6BAA6B,GAC7B,uBAAuB,GACvB,WAAW,GACX,oBAAoB,GACpB,kBAAkB,GAClB,qBAAqB,GACrB,mBAAmB,GACnB,4BAA4B,GAC5B,6BAA6B,GAC7B,iBAAiB,GACjB,6BAA6B,GAC7B,0BAA0B,GAC1B,mBAAmB,GACnB,2BAA2B,GAC3B,eAAe,GACf,QAAQ,CAAC;AAEb;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,KAAK,CAAC,EAAE,gBAAgB,CAAC;CAC1B;AAED;;GAEG;AACH,MAAM,WAAW,wBAAwB;IACvC,+CAA+C;IAC/C,OAAO,EAAE,OAAO,CAAC;IACjB,uEAAuE;IACvE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,gDAAgD;IAChD,KAAK,CAAC,EAAE,gBAAgB,CAAC;CAC1B;AAED;;GAEG;AACH,MAAM,MAAM,gBAAgB,GAAG;IAC7B,sCAAsC;IACtC,IAAI,EACA,qBAAqB,GACrB,eAAe,GACf,aAAa,GACb,qBAAqB,GACrB,cAAc,GACd,eAAe,CAAC;IACpB,mCAAmC;IACnC,OAAO,EAAE,MAAM,CAAC;IAChB,+BAA+B;IAC/B,OAAO,EAAE;QACP,2DAA2D;QAC3D,UAAU,EAAE,MAAM,CAAC;QACnB,8CAA8C;QAC9C,WAAW,EAAE,OAAO,CAAC;QACrB,+CAA+C;QAC/C,KAAK,CAAC,EAAE;YACN,6BAA6B;YAC7B,IAAI,EAAE,OAAO,GAAG,aAAa,CAAC;YAC9B,0CAA0C;YAC1C,QAAQ,CAAC,EAAE,MAAM,CAAC;YAClB,yDAAyD;YACzD,KAAK,CAAC,EAAE,MAAM,CAAC;YACf,0DAA0D;YAC1D,YAAY,CAAC,EAAE,MAAM,CAAC;YACtB,wDAAwD;YACxD,aAAa,CAAC,EAAE,MAAM,CAAC;SACxB,CAAC;KACH,CAAC;CACH,CAAA;AAED;;;GAGG;AACH,MAAM,WAAW,eAAe;IAC9B,sCAAsC;IACtC,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,0CAA0C;IAC1C,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,4CAA4C;IAC5C,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;GAEG;AACH,MAAM,WAAW,
|
|
1
|
+
{"version":3,"file":"SpotifyAuth.types.d.ts","sourceRoot":"","sources":["../src/SpotifyAuth.types.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { createContext } from "react";
|
|
2
2
|
export const SpotifyAuthContextInstance = createContext({
|
|
3
|
-
|
|
3
|
+
authState: {
|
|
4
|
+
accessToken: null,
|
|
5
|
+
refreshToken: null,
|
|
6
|
+
expiresIn: null,
|
|
7
|
+
tokenType: null,
|
|
8
|
+
scope: null,
|
|
9
|
+
},
|
|
4
10
|
authorize: async () => { },
|
|
5
11
|
isAuthenticating: false,
|
|
6
12
|
error: null,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SpotifyAuth.types.js","sourceRoot":"","sources":["../src/SpotifyAuth.types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"SpotifyAuth.types.js","sourceRoot":"","sources":["../src/SpotifyAuth.types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,OAAO,CAAC;AAiJtC,MAAM,CAAC,MAAM,0BAA0B,GAAG,aAAa,CAAqB;IAC1E,SAAS,EAAE;QACT,WAAW,EAAE,IAAI;QACjB,YAAY,EAAE,IAAI;QAClB,SAAS,EAAE,IAAI;QACf,SAAS,EAAE,IAAI;QACf,KAAK,EAAE,IAAI;KACZ;IACD,SAAS,EAAE,KAAK,IAAI,EAAE,GAAE,CAAC;IACzB,gBAAgB,EAAE,KAAK;IACvB,KAAK,EAAE,IAAI;CACZ,CAAC,CAAC","sourcesContent":["import { createContext } from \"react\";\n\n/**\n * Available Spotify authorization scopes.\n * @see https://developer.spotify.com/documentation/general/guides/authorization/scopes/\n */\nexport type SpotifyScopes =\n | 'app-remote-control'\n | 'playlist-modify-private'\n | 'playlist-modify-public'\n | 'playlist-read-collaborative'\n | 'playlist-read-private'\n | 'streaming'\n | 'user-follow-modify'\n | 'user-follow-read'\n | 'user-library-modify'\n | 'user-library-read'\n | 'user-modify-playback-state'\n | 'user-read-currently-playing'\n | 'user-read-email'\n | 'user-read-playback-position'\n | 'user-read-playback-state'\n | 'user-read-private'\n | 'user-read-recently-played'\n | 'user-top-read'\n | 'openid';\n\n/**\n * Event data structure for Spotify authorization events\n */\nexport interface SpotifyAuthEvent {\n success: boolean;\n token: string | null;\n refreshToken: string | null;\n expiresIn: number | null;\n tokenType: string | null;\n scope: string | null;\n error?: SpotifyAuthError;\n}\n\n/**\n * Data returned from the Spotify authorization process\n */\nexport interface SpotifyAuthorizationData {\n /** Whether the authorization was successful */\n success: boolean;\n /** The access token if authorization was successful, null otherwise */\n token: string | null;\n /** The refresh token if authorization was successful, null otherwise */\n refreshToken: string | null;\n /** The token expiration time in seconds if authorization was successful, null otherwise */\n expiresIn: number | null;\n /** The token type (e.g. \"Bearer\") if authorization was successful, null otherwise */\n tokenType: string | null;\n /** The granted scopes if authorization was successful, null otherwise */\n scope: string | null;\n /** Error information if authorization failed */\n error?: SpotifyAuthError;\n}\n\n/**\n * Possible error types that can occur during Spotify authentication\n */\nexport type SpotifyAuthError = {\n /** The type of error that occurred */\n type: \n | \"configuration_error\" // Missing or invalid configuration\n | \"network_error\" // Network-related issues\n | \"token_error\" // Issues with token exchange/refresh\n | \"authorization_error\" // User-facing authorization issues\n | \"server_error\" // Backend server issues\n | \"unknown_error\"; // Unexpected errors\n /** Human-readable error message */\n message: string;\n /** Additional error details */\n details: {\n /** Specific error code for more granular error handling */\n error_code: string;\n /** Whether the error can be recovered from */\n recoverable: boolean;\n /** Retry strategy information if applicable */\n retry?: {\n /** Type of retry strategy */\n type: \"fixed\" | \"exponential\";\n /** For fixed retry: number of attempts */\n attempts?: number;\n /** For fixed retry: delay between attempts in seconds */\n delay?: number;\n /** For exponential backoff: maximum number of attempts */\n max_attempts?: number;\n /** For exponential backoff: initial delay in seconds */\n initial_delay?: number;\n };\n };\n}\n\n/**\n * Configuration for the authorization request.\n * These are runtime options that can be changed between auth attempts.\n */\nexport interface AuthorizeConfig {\n /** Whether to show the auth dialog */\n showDialog?: boolean;\n /** Campaign identifier for attribution */\n campaign?: string;\n}\n\n/**\n * Props for the SpotifyAuthView component\n */\nexport interface SpotifyAuthViewProps {\n /** The name identifier for the auth view */\n name: string;\n}\n\n/**\n * Spotify authentication state containing all token-related information\n */\nexport interface SpotifyAuthState {\n /** The current Spotify access token, null if not authenticated */\n accessToken: string | null;\n /** The current refresh token, null if not authenticated */\n refreshToken: string | null;\n /** The token expiration time in seconds, null if not authenticated */\n expiresIn: number | null;\n /** The token type, null if not authenticated */\n tokenType: string | null;\n /** The token scope, null if not authenticated */\n scope: string | null;\n}\n\n/**\n * Context for Spotify authentication state and actions\n */\nexport interface SpotifyAuthContext {\n /** The complete Spotify authentication state */\n authState: SpotifyAuthState;\n /** Function to initiate Spotify authorization */\n authorize: (config: AuthorizeConfig) => Promise<void>;\n /** Whether authorization is in progress */\n isAuthenticating: boolean;\n /** Last error that occurred during authentication */\n error: SpotifyAuthError | null;\n}\n\nexport const SpotifyAuthContextInstance = createContext<SpotifyAuthContext>({\n authState: {\n accessToken: null,\n refreshToken: null,\n expiresIn: null,\n tokenType: null,\n scope: null,\n },\n authorize: async () => {},\n isAuthenticating: false,\n error: null,\n});\n\nexport interface SpotifyAuthOptions {\n clientId: string;\n redirectUrl: string;\n showDialog?: boolean;\n tokenRefreshFunction?: (data: SpotifyTokenResponse) => void;\n}\n\n/**\n * Response data from Spotify token endpoint\n */\nexport interface SpotifyTokenResponse {\n access_token: string;\n token_type: string;\n expires_in: number;\n refresh_token?: string;\n scope: string;\n}\n"]}
|
package/build/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.tsx"],"names":[],"mappings":"AACA,OAAO,KAAuD,MAAM,OAAO,CAAC;AAE5E,OAAO,EAEL,kBAAkB,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.tsx"],"names":[],"mappings":"AACA,OAAO,KAAuD,MAAM,OAAO,CAAC;AAE5E,OAAO,EAEL,kBAAkB,EAGlB,KAAK,eAAe,EAErB,MAAM,qBAAqB,CAAC;AAe7B;;GAEG;AACH,wBAAgB,SAAS,CAAC,MAAM,EAAE,eAAe,GAAG,IAAI,CAGvD;AAED,UAAU,wBAAwB;IAChC,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;CAC3B;AAED,wBAAgB,mBAAmB,CAAC,EAClC,QAAQ,GACT,EAAE,wBAAwB,GAAG,GAAG,CAAC,OAAO,CAwFxC;AAED,wBAAgB,cAAc,IAAI,kBAAkB,CAMnD"}
|
package/build/index.js
CHANGED
|
@@ -17,7 +17,13 @@ export function authorize(config) {
|
|
|
17
17
|
SpotifyAuthModule.authorize(config);
|
|
18
18
|
}
|
|
19
19
|
export function SpotifyAuthProvider({ children, }) {
|
|
20
|
-
const [
|
|
20
|
+
const [authState, setAuthState] = useState({
|
|
21
|
+
accessToken: null,
|
|
22
|
+
refreshToken: null,
|
|
23
|
+
expiresIn: null,
|
|
24
|
+
tokenType: null,
|
|
25
|
+
scope: null,
|
|
26
|
+
});
|
|
21
27
|
const [isAuthenticating, setIsAuthenticating] = useState(false);
|
|
22
28
|
const [error, setError] = useState(null);
|
|
23
29
|
const authorize = useCallback(async (config) => {
|
|
@@ -51,21 +57,41 @@ export function SpotifyAuthProvider({ children, }) {
|
|
|
51
57
|
useEffect(() => {
|
|
52
58
|
console.log('[SpotifyAuth] Setting up auth listener');
|
|
53
59
|
const subscription = addAuthListener((data) => {
|
|
54
|
-
console.log('[SpotifyAuth] Received auth event:', data
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
60
|
+
console.log('[SpotifyAuth] Received auth event:', JSON.stringify(data));
|
|
61
|
+
// Only update state if we receive a token
|
|
62
|
+
if (data.token) {
|
|
63
|
+
setAuthState({
|
|
64
|
+
accessToken: data.token,
|
|
65
|
+
refreshToken: data.refreshToken,
|
|
66
|
+
expiresIn: data.expiresIn,
|
|
67
|
+
tokenType: data.tokenType,
|
|
68
|
+
scope: data.scope,
|
|
69
|
+
});
|
|
70
|
+
setIsAuthenticating(false);
|
|
71
|
+
setError(null);
|
|
72
|
+
}
|
|
73
|
+
// Only set error if we have no token and there's an error
|
|
74
|
+
if (!data.token && data.error) {
|
|
58
75
|
console.error('[SpotifyAuth] Auth event error:', data.error);
|
|
59
|
-
|
|
76
|
+
setAuthState({
|
|
77
|
+
accessToken: null,
|
|
78
|
+
refreshToken: null,
|
|
79
|
+
expiresIn: null,
|
|
80
|
+
tokenType: null,
|
|
81
|
+
scope: null,
|
|
82
|
+
});
|
|
60
83
|
setError(data.error);
|
|
61
|
-
|
|
62
|
-
else {
|
|
63
|
-
setError(null);
|
|
84
|
+
setIsAuthenticating(false);
|
|
64
85
|
}
|
|
65
86
|
});
|
|
66
87
|
return () => subscription.remove();
|
|
67
88
|
}, []);
|
|
68
|
-
return (<SpotifyAuthContextInstance.Provider value={{
|
|
89
|
+
return (<SpotifyAuthContextInstance.Provider value={{
|
|
90
|
+
authState,
|
|
91
|
+
authorize,
|
|
92
|
+
isAuthenticating,
|
|
93
|
+
error
|
|
94
|
+
}}>
|
|
69
95
|
{children}
|
|
70
96
|
</SpotifyAuthContextInstance.Provider>);
|
|
71
97
|
}
|
package/build/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,KAAK,EAAE,EAAE,UAAU,EAAE,SAAS,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,OAAO,CAAC;AAE5E,OAAO,EAGL,0BAA0B,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.tsx"],"names":[],"mappings":"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","sourcesContent":["import { EventEmitter } from \"expo-modules-core\";\nimport React, { useContext, useEffect, useState, useCallback } from \"react\";\n\nimport {\n SpotifyAuthorizationData,\n SpotifyAuthContext,\n SpotifyAuthContextInstance,\n SpotifyAuthState,\n type AuthorizeConfig,\n type SpotifyAuthError,\n} from \"./SpotifyAuth.types\";\nimport SpotifyAuthModule from \"./SpotifyAuthModule\";\n\n// First define the event name as a string literal type\ntype SpotifyAuthEventName = \"onSpotifyAuth\"; // This should match SpotifyAuthModule.AuthEventName\n\n// Create a properly typed emitter\nconst emitter = new EventEmitter(SpotifyAuthModule);\n\nfunction addAuthListener(listener: (data: SpotifyAuthorizationData) => void) {\n // Assert the event name is of the correct type\n const eventName = SpotifyAuthModule.AuthEventName as SpotifyAuthEventName;\n return emitter.addListener(eventName, listener);\n}\n\n/**\n * Prompts the user to log in to Spotify and authorize your application.\n */\nexport function authorize(config: AuthorizeConfig): void {\n console.log('[SpotifyAuth] Initiating authorization request');\n SpotifyAuthModule.authorize(config);\n}\n\ninterface SpotifyAuthProviderProps {\n children: React.ReactNode;\n}\n\nexport function SpotifyAuthProvider({\n children,\n}: SpotifyAuthProviderProps): JSX.Element {\n const [authState, setAuthState] = useState<SpotifyAuthState>({\n accessToken: null,\n refreshToken: null,\n expiresIn: null,\n tokenType: null,\n scope: null,\n });\n const [isAuthenticating, setIsAuthenticating] = useState(false);\n const [error, setError] = useState<SpotifyAuthError | null>(null);\n\n const authorize = useCallback(\n async (config: AuthorizeConfig): Promise<void> => {\n try {\n console.log('[SpotifyAuth] Starting authorization process in provider');\n console.log('[SpotifyAuth] Authorization config:', JSON.stringify(config));\n setIsAuthenticating(true);\n setError(null);\n await SpotifyAuthModule.authorize(config);\n } catch (err) {\n console.error('[SpotifyAuth] Authorization error:', err);\n // Handle structured errors from the native layer\n if (err && typeof err === 'object' && 'type' in err) {\n setError(err as SpotifyAuthError);\n } else {\n // Create a generic error structure for unknown errors\n setError({\n type: 'unknown_error',\n message: err instanceof Error ? err.message : 'Authorization failed',\n details: {\n error_code: 'unknown',\n recoverable: false\n }\n });\n }\n throw err;\n }\n },\n [],\n );\n\n useEffect(() => {\n console.log('[SpotifyAuth] Setting up auth listener');\n const subscription = addAuthListener((data) => {\n console.log('[SpotifyAuth] Received auth event:', JSON.stringify(data));\n\n // Only update state if we receive a token\n if (data.token) {\n setAuthState({\n accessToken: data.token,\n refreshToken: data.refreshToken,\n expiresIn: data.expiresIn,\n tokenType: data.tokenType,\n scope: data.scope,\n });\n setIsAuthenticating(false);\n setError(null);\n }\n\n // Only set error if we have no token and there's an error\n if (!data.token && data.error) {\n console.error('[SpotifyAuth] Auth event error:', data.error);\n setAuthState({\n accessToken: null,\n refreshToken: null,\n expiresIn: null,\n tokenType: null,\n scope: null,\n });\n setError(data.error);\n setIsAuthenticating(false);\n }\n });\n return () => subscription.remove();\n }, []);\n\n return (\n <SpotifyAuthContextInstance.Provider\n value={{\n authState,\n authorize,\n isAuthenticating,\n error\n }}\n >\n {children}\n </SpotifyAuthContextInstance.Provider>\n );\n}\n\nexport function useSpotifyAuth(): SpotifyAuthContext {\n const context = useContext(SpotifyAuthContextInstance);\n if (!context) {\n throw new Error(\"useSpotifyAuth must be used within a SpotifyAuthProvider\");\n }\n return context;\n}\n"]}
|
|
@@ -7,15 +7,17 @@ struct SpotifySessionData {
|
|
|
7
7
|
let accessToken: String
|
|
8
8
|
let refreshToken: String
|
|
9
9
|
let expirationDate: Date
|
|
10
|
+
let scope: String?
|
|
10
11
|
|
|
11
12
|
var isExpired: Bool {
|
|
12
13
|
return Date() >= expirationDate
|
|
13
14
|
}
|
|
14
15
|
|
|
15
|
-
init(accessToken: String, refreshToken: String, expirationDate: Date) {
|
|
16
|
+
init(accessToken: String, refreshToken: String, expirationDate: Date, scope: String?) {
|
|
16
17
|
self.accessToken = accessToken
|
|
17
18
|
self.refreshToken = refreshToken
|
|
18
19
|
self.expirationDate = expirationDate
|
|
20
|
+
self.scope = scope
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
/// Initialize from an SPTSession (from app‑switch flow)
|
|
@@ -24,6 +26,7 @@ struct SpotifySessionData {
|
|
|
24
26
|
self.accessToken = session.accessToken
|
|
25
27
|
self.refreshToken = session.refreshToken
|
|
26
28
|
self.expirationDate = session.expirationDate
|
|
29
|
+
self.scope = session.scope?.scopesToStringArray().joined(separator: " ")
|
|
27
30
|
}
|
|
28
31
|
}
|
|
29
32
|
|
|
@@ -238,7 +241,8 @@ final class SpotifyAuthAuth: NSObject, SPTSessionManagerDelegate, SpotifyOAuthVi
|
|
|
238
241
|
|
|
239
242
|
private func securelyStoreToken(_ session: SpotifySessionData) {
|
|
240
243
|
// Pass token back to JS.
|
|
241
|
-
|
|
244
|
+
let expiresIn = session.expirationDate.timeIntervalSinceNow
|
|
245
|
+
module?.onAccessTokenObtained(session.accessToken, refreshToken: session.refreshToken, expiresIn: expiresIn, scope: session.scope, tokenType: "Bearer")
|
|
242
246
|
|
|
243
247
|
let refreshToken = session.refreshToken
|
|
244
248
|
if !refreshToken.isEmpty {
|
|
@@ -298,40 +302,74 @@ final class SpotifyAuthAuth: NSObject, SPTSessionManagerDelegate, SpotifyOAuthVi
|
|
|
298
302
|
request.httpMethod = "POST"
|
|
299
303
|
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
|
|
300
304
|
|
|
301
|
-
let params
|
|
302
|
-
do {
|
|
303
|
-
params = [
|
|
304
|
-
"grant_type": "refresh_token",
|
|
305
|
-
"refresh_token": currentSession.refreshToken,
|
|
306
|
-
"client_id": try self.clientID
|
|
307
|
-
]
|
|
308
|
-
} catch {
|
|
309
|
-
handleError(error, context: "token_refresh")
|
|
310
|
-
return
|
|
311
|
-
}
|
|
312
|
-
|
|
305
|
+
let params = ["refresh_token": currentSession.refreshToken]
|
|
313
306
|
let bodyString = params.map { "\($0)=\($1)" }.joined(separator: "&")
|
|
314
307
|
request.httpBody = bodyString.data(using: .utf8)
|
|
315
308
|
|
|
316
|
-
let task = URLSession.shared.dataTask(with: request) { [weak self] data,
|
|
309
|
+
let task = URLSession.shared.dataTask(with: request) { [weak self] data, response, error in
|
|
317
310
|
if let error = error {
|
|
318
|
-
self?.handleError(error, context: "token_refresh")
|
|
311
|
+
self?.handleError(SpotifyAuthError.networkError(error.localizedDescription), context: "token_refresh")
|
|
312
|
+
return
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
guard let httpResponse = response as? HTTPURLResponse else {
|
|
316
|
+
self?.handleError(SpotifyAuthError.networkError("Invalid response type"), context: "token_refresh")
|
|
317
|
+
return
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Check HTTP status code
|
|
321
|
+
guard (200...299).contains(httpResponse.statusCode) else {
|
|
322
|
+
let errorMessage: String
|
|
323
|
+
if let data = data, let errorJson = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
|
|
324
|
+
let errorDescription = errorJson["error_description"] as? String {
|
|
325
|
+
errorMessage = errorDescription
|
|
326
|
+
} else {
|
|
327
|
+
errorMessage = "Server returned status code \(httpResponse.statusCode)"
|
|
328
|
+
}
|
|
329
|
+
self?.handleError(SpotifyAuthError.networkError(errorMessage), context: "token_refresh")
|
|
319
330
|
return
|
|
320
331
|
}
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
let expiresIn = json["expires_in"] as? TimeInterval else {
|
|
325
|
-
self?.handleError(SpotifyAuthError.tokenError("Invalid token refresh response"), context: "token_refresh")
|
|
332
|
+
|
|
333
|
+
guard let data = data else {
|
|
334
|
+
self?.handleError(SpotifyAuthError.tokenError("No data received"), context: "token_refresh")
|
|
326
335
|
return
|
|
327
336
|
}
|
|
328
337
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
338
|
+
do {
|
|
339
|
+
let json = try JSONSerialization.jsonObject(with: data) as? [String: Any]
|
|
340
|
+
guard let json = json else {
|
|
341
|
+
throw SpotifyAuthError.tokenError("Invalid JSON response")
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Extract and validate required fields
|
|
345
|
+
guard let accessToken = json["access_token"] as? String else {
|
|
346
|
+
throw SpotifyAuthError.tokenError("Missing access_token in response")
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
guard let expiresInString = json["expires_in"] as? String,
|
|
350
|
+
let expiresIn = TimeInterval(expiresInString) else {
|
|
351
|
+
throw SpotifyAuthError.tokenError("Invalid or missing expires_in in response")
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
guard let tokenType = json["token_type"] as? String,
|
|
355
|
+
tokenType.lowercased() == "bearer" else {
|
|
356
|
+
throw SpotifyAuthError.tokenError("Invalid or missing token_type in response")
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Optional field
|
|
360
|
+
let scope = json["scope"] as? String
|
|
361
|
+
|
|
362
|
+
// Keep the existing refresh token since server doesn't send a new one
|
|
363
|
+
let refreshToken = currentSession.refreshToken
|
|
364
|
+
let expirationDate = Date(timeIntervalSinceNow: expiresIn)
|
|
365
|
+
let newSession = SpotifySessionData(accessToken: accessToken, refreshToken: refreshToken, expirationDate: expirationDate, scope: scope)
|
|
366
|
+
|
|
367
|
+
DispatchQueue.main.async {
|
|
368
|
+
self?.currentSession = newSession
|
|
369
|
+
self?.module?.onAccessTokenObtained(accessToken, refreshToken: refreshToken, expiresIn: expiresIn, scope: scope, tokenType: tokenType)
|
|
370
|
+
}
|
|
371
|
+
} catch {
|
|
372
|
+
self?.handleError(error, context: "token_refresh")
|
|
335
373
|
}
|
|
336
374
|
}
|
|
337
375
|
task.resume()
|
|
@@ -489,10 +527,8 @@ final class SpotifyAuthAuth: NSObject, SPTSessionManagerDelegate, SpotifyOAuthVi
|
|
|
489
527
|
let params: [String: String]
|
|
490
528
|
do {
|
|
491
529
|
params = [
|
|
492
|
-
"grant_type": "authorization_code",
|
|
493
530
|
"code": code,
|
|
494
|
-
"redirect_uri": try self.redirectURL.absoluteString
|
|
495
|
-
"client_id": try self.clientID
|
|
531
|
+
"redirect_uri": try self.redirectURL.absoluteString
|
|
496
532
|
]
|
|
497
533
|
} catch {
|
|
498
534
|
handleError(error, context: "token_exchange")
|
|
@@ -533,18 +569,37 @@ final class SpotifyAuthAuth: NSObject, SPTSessionManagerDelegate, SpotifyOAuthVi
|
|
|
533
569
|
|
|
534
570
|
do {
|
|
535
571
|
let json = try JSONSerialization.jsonObject(with: data) as? [String: Any]
|
|
536
|
-
guard let json = json
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
572
|
+
guard let json = json else {
|
|
573
|
+
throw SpotifyAuthError.tokenError("Invalid JSON response")
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// Extract and validate required fields
|
|
577
|
+
guard let accessToken = json["access_token"] as? String else {
|
|
578
|
+
throw SpotifyAuthError.tokenError("Missing access_token in response")
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
guard let refreshToken = json["refresh_token"] as? String else {
|
|
582
|
+
throw SpotifyAuthError.tokenError("Missing refresh_token in response")
|
|
541
583
|
}
|
|
542
584
|
|
|
585
|
+
guard let expiresInString = json["expires_in"] as? String,
|
|
586
|
+
let expiresIn = TimeInterval(expiresInString) else {
|
|
587
|
+
throw SpotifyAuthError.tokenError("Invalid or missing expires_in in response")
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
guard let tokenType = json["token_type"] as? String,
|
|
591
|
+
tokenType.lowercased() == "bearer" else {
|
|
592
|
+
throw SpotifyAuthError.tokenError("Invalid or missing token_type in response")
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// Optional field
|
|
596
|
+
let scope = json["scope"] as? String
|
|
597
|
+
|
|
543
598
|
let expirationDate = Date(timeIntervalSinceNow: expiresIn)
|
|
544
|
-
let sessionData = SpotifySessionData(accessToken: accessToken, refreshToken: refreshToken, expirationDate: expirationDate)
|
|
599
|
+
let sessionData = SpotifySessionData(accessToken: accessToken, refreshToken: refreshToken, expirationDate: expirationDate, scope: scope)
|
|
545
600
|
DispatchQueue.main.async {
|
|
546
601
|
self?.currentSession = sessionData
|
|
547
|
-
self?.module?.onAccessTokenObtained(accessToken)
|
|
602
|
+
self?.module?.onAccessTokenObtained(accessToken, refreshToken: refreshToken, expiresIn: expiresIn, scope: scope, tokenType: tokenType)
|
|
548
603
|
}
|
|
549
604
|
} catch {
|
|
550
605
|
self?.handleError(error, context: "token_exchange")
|
|
@@ -26,10 +26,11 @@ struct AuthorizeConfig: Record {
|
|
|
26
26
|
|
|
27
27
|
// Define a private enum for mapping Spotify SDK error codes.
|
|
28
28
|
// (The raw values here are examples; adjust them to match your SDK's definitions.)
|
|
29
|
-
private enum SPTErrorCode:
|
|
30
|
-
case
|
|
31
|
-
case
|
|
32
|
-
case
|
|
29
|
+
private enum SPTErrorCode: UInt {
|
|
30
|
+
case unknown = 0
|
|
31
|
+
case authorizationFailed = 1
|
|
32
|
+
case renewSessionFailed = 2
|
|
33
|
+
case jsonFailed = 3
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
public class SpotifyAuthModule: Module {
|
|
@@ -78,7 +79,7 @@ public class SpotifyAuthModule: Module {
|
|
|
78
79
|
View(SpotifyOAuthView.self) {
|
|
79
80
|
Events(spotifyAuthorizationEventName)
|
|
80
81
|
|
|
81
|
-
Prop("name") { (
|
|
82
|
+
Prop("name") { (_: SpotifyOAuthView, _: String) in
|
|
82
83
|
DispatchQueue.main.async {
|
|
83
84
|
secureLog("View prop updated")
|
|
84
85
|
}
|
|
@@ -87,14 +88,13 @@ public class SpotifyAuthModule: Module {
|
|
|
87
88
|
}
|
|
88
89
|
|
|
89
90
|
private func sanitizeErrorMessage(_ message: String) -> String {
|
|
90
|
-
//
|
|
91
|
+
// Only redact actual sensitive values, not general terms
|
|
91
92
|
let sensitivePatterns = [
|
|
92
|
-
"(?i)client[_-]?id",
|
|
93
|
-
"(?i)
|
|
94
|
-
"(?i)
|
|
95
|
-
"(?i)
|
|
96
|
-
"(?i)
|
|
97
|
-
"(?i)password"
|
|
93
|
+
"(?i)client[_-]?id=[^&\\s]+",
|
|
94
|
+
"(?i)access_token=[^&\\s]+",
|
|
95
|
+
"(?i)refresh_token=[^&\\s]+",
|
|
96
|
+
"(?i)secret=[^&\\s]+",
|
|
97
|
+
"(?i)api[_-]?key=[^&\\s]+"
|
|
98
98
|
]
|
|
99
99
|
|
|
100
100
|
var sanitized = message
|
|
@@ -103,7 +103,7 @@ public class SpotifyAuthModule: Module {
|
|
|
103
103
|
sanitized = regex.stringByReplacingMatches(
|
|
104
104
|
in: sanitized,
|
|
105
105
|
range: NSRange(sanitized.startIndex..., in: sanitized),
|
|
106
|
-
withTemplate: "[REDACTED]"
|
|
106
|
+
withTemplate: "$1[REDACTED]"
|
|
107
107
|
)
|
|
108
108
|
}
|
|
109
109
|
}
|
|
@@ -111,11 +111,15 @@ public class SpotifyAuthModule: Module {
|
|
|
111
111
|
}
|
|
112
112
|
|
|
113
113
|
@objc
|
|
114
|
-
public func onAccessTokenObtained(_ token: String) {
|
|
114
|
+
public func onAccessTokenObtained(_ token: String, refreshToken: String, expiresIn: TimeInterval, scope: String?, tokenType: String) {
|
|
115
115
|
secureLog("Access token obtained", sensitive: true)
|
|
116
116
|
let eventData: [String: Any] = [
|
|
117
117
|
"success": true,
|
|
118
118
|
"token": token,
|
|
119
|
+
"refreshToken": refreshToken,
|
|
120
|
+
"expiresIn": expiresIn,
|
|
121
|
+
"tokenType": tokenType,
|
|
122
|
+
"scope": scope as Any,
|
|
119
123
|
"error": NSNull() // Use NSNull() instead of nil.
|
|
120
124
|
]
|
|
121
125
|
sendEvent(spotifyAuthorizationEventName, eventData)
|
|
@@ -126,30 +130,42 @@ public class SpotifyAuthModule: Module {
|
|
|
126
130
|
secureLog("User signed out")
|
|
127
131
|
let eventData: [String: Any] = [
|
|
128
132
|
"success": true,
|
|
129
|
-
"token": NSNull(),
|
|
130
|
-
"
|
|
133
|
+
"token": NSNull(),
|
|
134
|
+
"refreshToken": NSNull(),
|
|
135
|
+
"expiresIn": NSNull(),
|
|
136
|
+
"tokenType": NSNull(),
|
|
137
|
+
"scope": NSNull(),
|
|
138
|
+
"error": NSNull()
|
|
131
139
|
]
|
|
132
140
|
sendEvent(spotifyAuthorizationEventName, eventData)
|
|
133
141
|
}
|
|
134
142
|
|
|
135
143
|
@objc
|
|
136
144
|
public func onAuthorizationError(_ error: Error) {
|
|
145
|
+
// Skip sending error events for expected state transitions
|
|
146
|
+
if let spotifyError = error as? SpotifyAuthError,
|
|
147
|
+
case .sessionError(let message) = spotifyError,
|
|
148
|
+
message.contains("authentication process") ||
|
|
149
|
+
message.contains("token exchange") {
|
|
150
|
+
// This is likely a state transition, not an error
|
|
151
|
+
secureLog("Auth state transition: \(message)")
|
|
152
|
+
return
|
|
153
|
+
}
|
|
154
|
+
|
|
137
155
|
let errorData: [String: Any]
|
|
138
156
|
|
|
139
157
|
if let spotifyError = error as? SpotifyAuthError {
|
|
140
|
-
// Map domain error to a structured format
|
|
141
158
|
errorData = mapSpotifyError(spotifyError)
|
|
142
159
|
} else if let sptError = error as? SPTError {
|
|
143
|
-
// Map Spotify SDK errors
|
|
144
160
|
errorData = mapSPTError(sptError)
|
|
145
161
|
} else {
|
|
146
|
-
// Map unknown errors
|
|
147
162
|
errorData = [
|
|
148
163
|
"type": "unknown_error",
|
|
149
164
|
"message": sanitizeErrorMessage(error.localizedDescription),
|
|
150
165
|
"details": [
|
|
151
166
|
"error_code": "unknown",
|
|
152
|
-
"recoverable": false
|
|
167
|
+
"recoverable": false,
|
|
168
|
+
"error_type": String(describing: type(of: error))
|
|
153
169
|
]
|
|
154
170
|
]
|
|
155
171
|
}
|
|
@@ -159,6 +175,10 @@ public class SpotifyAuthModule: Module {
|
|
|
159
175
|
let eventData: [String: Any] = [
|
|
160
176
|
"success": false,
|
|
161
177
|
"token": NSNull(),
|
|
178
|
+
"refreshToken": NSNull(),
|
|
179
|
+
"expiresIn": NSNull(),
|
|
180
|
+
"tokenType": NSNull(),
|
|
181
|
+
"scope": NSNull(),
|
|
162
182
|
"error": errorData
|
|
163
183
|
]
|
|
164
184
|
sendEvent(spotifyAuthorizationEventName, eventData)
|
|
@@ -198,21 +218,30 @@ public class SpotifyAuthModule: Module {
|
|
|
198
218
|
|
|
199
219
|
private func mapSPTError(_ error: SPTError) -> [String: Any] {
|
|
200
220
|
let message = sanitizeErrorMessage(error.localizedDescription)
|
|
201
|
-
|
|
221
|
+
var details: [String: Any] = [
|
|
202
222
|
"error_code": error.code,
|
|
203
223
|
"recoverable": false
|
|
204
224
|
]
|
|
205
225
|
|
|
226
|
+
// Add underlying error info if available
|
|
227
|
+
if let underlying = error.userInfo[NSUnderlyingErrorKey] as? Error {
|
|
228
|
+
details["underlying_error"] = underlying.localizedDescription
|
|
229
|
+
}
|
|
230
|
+
|
|
206
231
|
let type: String
|
|
207
|
-
switch error.code {
|
|
208
|
-
case
|
|
232
|
+
switch SPTErrorCode(rawValue: UInt(error.code)) {
|
|
233
|
+
case .authorizationFailed:
|
|
209
234
|
type = "authorization_error"
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
235
|
+
details["recoverable"] = true // Auth failures are usually recoverable
|
|
236
|
+
case .renewSessionFailed:
|
|
237
|
+
type = "session_error" // Changed from token_error to be more specific
|
|
238
|
+
details["recoverable"] = true
|
|
239
|
+
case .jsonFailed:
|
|
213
240
|
type = "server_error"
|
|
214
|
-
|
|
241
|
+
details["recoverable"] = false
|
|
242
|
+
case .unknown, .none:
|
|
215
243
|
type = "unknown_error"
|
|
244
|
+
details["recoverable"] = false
|
|
216
245
|
}
|
|
217
246
|
|
|
218
247
|
return [
|
|
@@ -231,11 +260,16 @@ public class SpotifyAuthModule: Module {
|
|
|
231
260
|
case .tokenError:
|
|
232
261
|
return ("token_error", "token_invalid")
|
|
233
262
|
case .sessionError:
|
|
234
|
-
return ("
|
|
263
|
+
return ("session_error", "session_error") // Changed from authorization_error
|
|
235
264
|
case .networkError:
|
|
236
265
|
return ("network_error", "network_failed")
|
|
237
|
-
case .recoverable:
|
|
238
|
-
|
|
266
|
+
case .recoverable(let baseError, _):
|
|
267
|
+
// Use the base error type but mark as recoverable in the details
|
|
268
|
+
if let spotifyError = baseError as? SpotifyAuthError {
|
|
269
|
+
let (type, code) = classifySpotifyError(spotifyError)
|
|
270
|
+
return (type, "recoverable_\(code)")
|
|
271
|
+
}
|
|
272
|
+
return ("recoverable_error", "recoverable_unknown")
|
|
239
273
|
}
|
|
240
274
|
}
|
|
241
275
|
|
package/package.json
CHANGED
package/src/SpotifyAuth.types.ts
CHANGED
|
@@ -31,6 +31,10 @@ export type SpotifyScopes =
|
|
|
31
31
|
export interface SpotifyAuthEvent {
|
|
32
32
|
success: boolean;
|
|
33
33
|
token: string | null;
|
|
34
|
+
refreshToken: string | null;
|
|
35
|
+
expiresIn: number | null;
|
|
36
|
+
tokenType: string | null;
|
|
37
|
+
scope: string | null;
|
|
34
38
|
error?: SpotifyAuthError;
|
|
35
39
|
}
|
|
36
40
|
|
|
@@ -42,6 +46,14 @@ export interface SpotifyAuthorizationData {
|
|
|
42
46
|
success: boolean;
|
|
43
47
|
/** The access token if authorization was successful, null otherwise */
|
|
44
48
|
token: string | null;
|
|
49
|
+
/** The refresh token if authorization was successful, null otherwise */
|
|
50
|
+
refreshToken: string | null;
|
|
51
|
+
/** The token expiration time in seconds if authorization was successful, null otherwise */
|
|
52
|
+
expiresIn: number | null;
|
|
53
|
+
/** The token type (e.g. "Bearer") if authorization was successful, null otherwise */
|
|
54
|
+
tokenType: string | null;
|
|
55
|
+
/** The granted scopes if authorization was successful, null otherwise */
|
|
56
|
+
scope: string | null;
|
|
45
57
|
/** Error information if authorization failed */
|
|
46
58
|
error?: SpotifyAuthError;
|
|
47
59
|
}
|
|
@@ -102,11 +114,27 @@ export interface SpotifyAuthViewProps {
|
|
|
102
114
|
}
|
|
103
115
|
|
|
104
116
|
/**
|
|
105
|
-
*
|
|
117
|
+
* Spotify authentication state containing all token-related information
|
|
106
118
|
*/
|
|
107
|
-
export interface
|
|
119
|
+
export interface SpotifyAuthState {
|
|
108
120
|
/** The current Spotify access token, null if not authenticated */
|
|
109
121
|
accessToken: string | null;
|
|
122
|
+
/** The current refresh token, null if not authenticated */
|
|
123
|
+
refreshToken: string | null;
|
|
124
|
+
/** The token expiration time in seconds, null if not authenticated */
|
|
125
|
+
expiresIn: number | null;
|
|
126
|
+
/** The token type, null if not authenticated */
|
|
127
|
+
tokenType: string | null;
|
|
128
|
+
/** The token scope, null if not authenticated */
|
|
129
|
+
scope: string | null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Context for Spotify authentication state and actions
|
|
134
|
+
*/
|
|
135
|
+
export interface SpotifyAuthContext {
|
|
136
|
+
/** The complete Spotify authentication state */
|
|
137
|
+
authState: SpotifyAuthState;
|
|
110
138
|
/** Function to initiate Spotify authorization */
|
|
111
139
|
authorize: (config: AuthorizeConfig) => Promise<void>;
|
|
112
140
|
/** Whether authorization is in progress */
|
|
@@ -116,7 +144,13 @@ export interface SpotifyAuthContext {
|
|
|
116
144
|
}
|
|
117
145
|
|
|
118
146
|
export const SpotifyAuthContextInstance = createContext<SpotifyAuthContext>({
|
|
119
|
-
|
|
147
|
+
authState: {
|
|
148
|
+
accessToken: null,
|
|
149
|
+
refreshToken: null,
|
|
150
|
+
expiresIn: null,
|
|
151
|
+
tokenType: null,
|
|
152
|
+
scope: null,
|
|
153
|
+
},
|
|
120
154
|
authorize: async () => {},
|
|
121
155
|
isAuthenticating: false,
|
|
122
156
|
error: null,
|
package/src/index.tsx
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
SpotifyAuthorizationData,
|
|
6
6
|
SpotifyAuthContext,
|
|
7
7
|
SpotifyAuthContextInstance,
|
|
8
|
+
SpotifyAuthState,
|
|
8
9
|
type AuthorizeConfig,
|
|
9
10
|
type SpotifyAuthError,
|
|
10
11
|
} from "./SpotifyAuth.types";
|
|
@@ -37,7 +38,13 @@ interface SpotifyAuthProviderProps {
|
|
|
37
38
|
export function SpotifyAuthProvider({
|
|
38
39
|
children,
|
|
39
40
|
}: SpotifyAuthProviderProps): JSX.Element {
|
|
40
|
-
const [
|
|
41
|
+
const [authState, setAuthState] = useState<SpotifyAuthState>({
|
|
42
|
+
accessToken: null,
|
|
43
|
+
refreshToken: null,
|
|
44
|
+
expiresIn: null,
|
|
45
|
+
tokenType: null,
|
|
46
|
+
scope: null,
|
|
47
|
+
});
|
|
41
48
|
const [isAuthenticating, setIsAuthenticating] = useState(false);
|
|
42
49
|
const [error, setError] = useState<SpotifyAuthError | null>(null);
|
|
43
50
|
|
|
@@ -74,16 +81,33 @@ export function SpotifyAuthProvider({
|
|
|
74
81
|
useEffect(() => {
|
|
75
82
|
console.log('[SpotifyAuth] Setting up auth listener');
|
|
76
83
|
const subscription = addAuthListener((data) => {
|
|
77
|
-
console.log('[SpotifyAuth] Received auth event:', data
|
|
78
|
-
setToken(data.token);
|
|
79
|
-
setIsAuthenticating(false);
|
|
84
|
+
console.log('[SpotifyAuth] Received auth event:', JSON.stringify(data));
|
|
80
85
|
|
|
81
|
-
if
|
|
86
|
+
// Only update state if we receive a token
|
|
87
|
+
if (data.token) {
|
|
88
|
+
setAuthState({
|
|
89
|
+
accessToken: data.token,
|
|
90
|
+
refreshToken: data.refreshToken,
|
|
91
|
+
expiresIn: data.expiresIn,
|
|
92
|
+
tokenType: data.tokenType,
|
|
93
|
+
scope: data.scope,
|
|
94
|
+
});
|
|
95
|
+
setIsAuthenticating(false);
|
|
96
|
+
setError(null);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Only set error if we have no token and there's an error
|
|
100
|
+
if (!data.token && data.error) {
|
|
82
101
|
console.error('[SpotifyAuth] Auth event error:', data.error);
|
|
83
|
-
|
|
102
|
+
setAuthState({
|
|
103
|
+
accessToken: null,
|
|
104
|
+
refreshToken: null,
|
|
105
|
+
expiresIn: null,
|
|
106
|
+
tokenType: null,
|
|
107
|
+
scope: null,
|
|
108
|
+
});
|
|
84
109
|
setError(data.error);
|
|
85
|
-
|
|
86
|
-
setError(null);
|
|
110
|
+
setIsAuthenticating(false);
|
|
87
111
|
}
|
|
88
112
|
});
|
|
89
113
|
return () => subscription.remove();
|
|
@@ -91,7 +115,12 @@ export function SpotifyAuthProvider({
|
|
|
91
115
|
|
|
92
116
|
return (
|
|
93
117
|
<SpotifyAuthContextInstance.Provider
|
|
94
|
-
value={{
|
|
118
|
+
value={{
|
|
119
|
+
authState,
|
|
120
|
+
authorize,
|
|
121
|
+
isAuthenticating,
|
|
122
|
+
error
|
|
123
|
+
}}
|
|
95
124
|
>
|
|
96
125
|
{children}
|
|
97
126
|
</SpotifyAuthContextInstance.Provider>
|