@superfan-app/spotify-auth 0.1.35 → 0.1.37
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 +33 -4
- package/build/SpotifyAuth.types.d.ts.map +1 -1
- package/build/SpotifyAuth.types.js.map +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +21 -5
- package/build/index.js.map +1 -1
- package/ios/SpotifyAuthAuth.swift +68 -35
- package/ios/SpotifyAuthModule.swift +101 -6
- package/package.json +2 -2
- package/src/SpotifyAuth.types.ts +40 -4
- package/src/index.tsx +22 -5
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
export interface SpotifyAuthEvent {
|
|
5
5
|
success: boolean;
|
|
6
6
|
token: string | null;
|
|
7
|
-
error?:
|
|
7
|
+
error?: SpotifyAuthError;
|
|
8
8
|
}
|
|
9
9
|
/**
|
|
10
10
|
* Data returned from the Spotify authorization process
|
|
@@ -14,9 +14,38 @@ export interface SpotifyAuthorizationData {
|
|
|
14
14
|
success: boolean;
|
|
15
15
|
/** The access token if authorization was successful, null otherwise */
|
|
16
16
|
token: string | null;
|
|
17
|
-
/** Error
|
|
18
|
-
error?:
|
|
17
|
+
/** Error information if authorization failed */
|
|
18
|
+
error?: SpotifyAuthError;
|
|
19
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* Possible error types that can occur during Spotify authentication
|
|
22
|
+
*/
|
|
23
|
+
export type SpotifyAuthError = {
|
|
24
|
+
/** The type of error that occurred */
|
|
25
|
+
type: "configuration_error" | "network_error" | "token_error" | "authorization_error" | "server_error" | "unknown_error";
|
|
26
|
+
/** Human-readable error message */
|
|
27
|
+
message: string;
|
|
28
|
+
/** Additional error details */
|
|
29
|
+
details: {
|
|
30
|
+
/** Specific error code for more granular error handling */
|
|
31
|
+
error_code: string;
|
|
32
|
+
/** Whether the error can be recovered from */
|
|
33
|
+
recoverable: boolean;
|
|
34
|
+
/** Retry strategy information if applicable */
|
|
35
|
+
retry?: {
|
|
36
|
+
/** Type of retry strategy */
|
|
37
|
+
type: "fixed" | "exponential";
|
|
38
|
+
/** For fixed retry: number of attempts */
|
|
39
|
+
attempts?: number;
|
|
40
|
+
/** For fixed retry: delay between attempts in seconds */
|
|
41
|
+
delay?: number;
|
|
42
|
+
/** For exponential backoff: maximum number of attempts */
|
|
43
|
+
max_attempts?: number;
|
|
44
|
+
/** For exponential backoff: initial delay in seconds */
|
|
45
|
+
initial_delay?: number;
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
};
|
|
20
49
|
/**
|
|
21
50
|
* Configuration for the authorization request
|
|
22
51
|
*/
|
|
@@ -48,7 +77,7 @@ export interface SpotifyAuthContext {
|
|
|
48
77
|
/** Whether authorization is in progress */
|
|
49
78
|
isAuthenticating: boolean;
|
|
50
79
|
/** Last error that occurred during authentication */
|
|
51
|
-
error:
|
|
80
|
+
error: SpotifyAuthError | null;
|
|
52
81
|
}
|
|
53
82
|
export declare const SpotifyAuthContextInstance: import("react").Context<SpotifyAuthContext>;
|
|
54
83
|
export interface SpotifyAuthOptions {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SpotifyAuth.types.d.ts","sourceRoot":"","sources":["../src/SpotifyAuth.types.ts"],"names":[],"mappings":"AAEA;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,KAAK,CAAC,EAAE,
|
|
1
|
+
{"version":3,"file":"SpotifyAuth.types.d.ts","sourceRoot":"","sources":["../src/SpotifyAuth.types.ts"],"names":[],"mappings":"AAEA;;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;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,wBAAwB;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,yBAAyB;IACzB,WAAW,EAAE,MAAM,CAAC;IACpB,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,kBAAkB;IACjC,kEAAkE;IAClE,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,iDAAiD;IACjD,SAAS,EAAE,CAAC,MAAM,EAAE,eAAe,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACtD,2CAA2C;IAC3C,gBAAgB,EAAE,OAAO,CAAC;IAC1B,qDAAqD;IACrD,KAAK,EAAE,gBAAgB,GAAG,IAAI,CAAC;CAChC;AAED,eAAO,MAAM,0BAA0B,6CAKrC,CAAC;AAEH,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,oBAAoB,CAAC,EAAE,CAAC,IAAI,EAAE,oBAAoB,KAAK,IAAI,CAAC;CAC7D;AAED;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC;CACf"}
|
|
@@ -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;AA+FtC,MAAM,CAAC,MAAM,0BAA0B,GAAG,aAAa,CAAqB;IAC1E,WAAW,EAAE,IAAI;IACjB,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 * Event data structure for Spotify authorization events\n */\nexport interface SpotifyAuthEvent {\n success: boolean;\n token: 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 /** 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 */\nexport interface AuthorizeConfig {\n /** Spotify Client ID */\n clientId: string;\n /** OAuth redirect URL */\n redirectUrl: string;\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 * Context for Spotify authentication state and actions\n */\nexport interface SpotifyAuthContext {\n /** The current Spotify access token, null if not authenticated */\n accessToken: string | null;\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 accessToken: null,\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,EAElB,KAAK,eAAe,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.tsx"],"names":[],"mappings":"AACA,OAAO,KAAuD,MAAM,OAAO,CAAC;AAE5E,OAAO,EAEL,kBAAkB,EAElB,KAAK,eAAe,EAErB,MAAM,qBAAqB,CAAC;AAe7B;;GAEG;AACH,wBAAgB,SAAS,CAAC,MAAM,EAAE,eAAe,GAAG,IAAI,CAEvD;AAED,UAAU,wBAAwB;IAChC,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;CAC3B;AAED,wBAAgB,mBAAmB,CAAC,EAClC,QAAQ,GACT,EAAE,wBAAwB,GAAG,GAAG,CAAC,OAAO,CAsDxC;AAED,wBAAgB,cAAc,IAAI,kBAAkB,CAMnD"}
|
package/build/index.js
CHANGED
|
@@ -26,18 +26,34 @@ export function SpotifyAuthProvider({ children, }) {
|
|
|
26
26
|
await SpotifyAuthModule.authorize(config);
|
|
27
27
|
}
|
|
28
28
|
catch (err) {
|
|
29
|
-
|
|
29
|
+
// Handle structured errors from the native layer
|
|
30
|
+
if (err && typeof err === 'object' && 'type' in err) {
|
|
31
|
+
setError(err);
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
// Create a generic error structure for unknown errors
|
|
35
|
+
setError({
|
|
36
|
+
type: 'unknown_error',
|
|
37
|
+
message: err instanceof Error ? err.message : 'Authorization failed',
|
|
38
|
+
details: {
|
|
39
|
+
error_code: 'unknown',
|
|
40
|
+
recoverable: false
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
}
|
|
30
44
|
throw err;
|
|
31
45
|
}
|
|
32
|
-
finally {
|
|
33
|
-
setIsAuthenticating(false);
|
|
34
|
-
}
|
|
35
46
|
}, []);
|
|
36
47
|
useEffect(() => {
|
|
37
48
|
const subscription = addAuthListener((data) => {
|
|
38
49
|
setToken(data.token);
|
|
50
|
+
setIsAuthenticating(false);
|
|
39
51
|
if (data.error) {
|
|
40
|
-
console.error(
|
|
52
|
+
console.error('Spotify auth error:', data.error);
|
|
53
|
+
setError(data.error);
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
setError(null);
|
|
41
57
|
}
|
|
42
58
|
});
|
|
43
59
|
return () => subscription.remove();
|
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 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 SpotifyAuthModule.authorize(config);\n}\n\ninterface SpotifyAuthProviderProps {\n children: React.ReactNode;\n}\n\nexport function SpotifyAuthProvider({\n children,\n}: SpotifyAuthProviderProps): JSX.Element {\n const [token, setToken] = useState<string | null>(null);\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 setIsAuthenticating(true);\n setError(null);\n await SpotifyAuthModule.authorize(config);\n } catch (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 const subscription = addAuthListener((data) => {\n setToken(data.token);\n setIsAuthenticating(false);\n\n if (data.error) {\n console.error('Spotify auth error:', data.error);\n setError(data.error);\n } else {\n setError(null);\n }\n });\n return () => subscription.remove();\n }, []);\n\n return (\n <SpotifyAuthContextInstance.Provider\n value={{ accessToken: token, authorize, isAuthenticating, error }}\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"]}
|
|
@@ -89,7 +89,7 @@ enum SpotifyAuthError: Error {
|
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
final class SpotifyAuthAuth: NSObject, SPTSessionManagerDelegate, SpotifyOAuthViewDelegate {
|
|
92
|
-
/// A weak reference to our module
|
|
92
|
+
/// A weak reference to our module's JS interface.
|
|
93
93
|
weak var module: SpotifyAuthModule?
|
|
94
94
|
|
|
95
95
|
/// For web‑auth we present our own OAuth view.
|
|
@@ -489,8 +489,8 @@ final class SpotifyAuthAuth: NSObject, SPTSessionManagerDelegate, SpotifyOAuthVi
|
|
|
489
489
|
private func exchangeCodeForToken(_ code: String) {
|
|
490
490
|
guard let swapURLString = try? self.tokenSwapURL,
|
|
491
491
|
let url = URL(string: swapURLString) else {
|
|
492
|
-
|
|
493
|
-
|
|
492
|
+
handleError(SpotifyAuthError.invalidConfiguration("Invalid token swap URL"), context: "token_exchange")
|
|
493
|
+
return
|
|
494
494
|
}
|
|
495
495
|
|
|
496
496
|
var request = URLRequest(url: url)
|
|
@@ -499,40 +499,67 @@ final class SpotifyAuthAuth: NSObject, SPTSessionManagerDelegate, SpotifyOAuthVi
|
|
|
499
499
|
|
|
500
500
|
let params: [String: String]
|
|
501
501
|
do {
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
502
|
+
params = [
|
|
503
|
+
"grant_type": "authorization_code",
|
|
504
|
+
"code": code,
|
|
505
|
+
"redirect_uri": try self.redirectURL.absoluteString,
|
|
506
|
+
"client_id": try self.clientID
|
|
507
|
+
]
|
|
508
508
|
} catch {
|
|
509
|
-
|
|
510
|
-
|
|
509
|
+
handleError(error, context: "token_exchange")
|
|
510
|
+
return
|
|
511
511
|
}
|
|
512
512
|
|
|
513
513
|
let bodyString = params.map { "\($0)=\($1)" }.joined(separator: "&")
|
|
514
514
|
request.httpBody = bodyString.data(using: .utf8)
|
|
515
515
|
|
|
516
516
|
let task = URLSession.shared.dataTask(with: request) { [weak self] data, response, error in
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
517
|
+
if let error = error {
|
|
518
|
+
self?.handleError(SpotifyAuthError.networkError(error.localizedDescription), context: "token_exchange")
|
|
519
|
+
return
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
guard let httpResponse = response as? HTTPURLResponse else {
|
|
523
|
+
self?.handleError(SpotifyAuthError.networkError("Invalid response type"), context: "token_exchange")
|
|
524
|
+
return
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// Check HTTP status code
|
|
528
|
+
guard (200...299).contains(httpResponse.statusCode) else {
|
|
529
|
+
let errorMessage: String
|
|
530
|
+
if let data = data, let errorJson = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
|
|
531
|
+
let errorDescription = errorJson["error_description"] as? String {
|
|
532
|
+
errorMessage = errorDescription
|
|
533
|
+
} else {
|
|
534
|
+
errorMessage = "Server returned status code \(httpResponse.statusCode)"
|
|
535
|
+
}
|
|
536
|
+
self?.handleError(SpotifyAuthError.networkError(errorMessage), context: "token_exchange")
|
|
537
|
+
return
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
guard let data = data else {
|
|
541
|
+
self?.handleError(SpotifyAuthError.tokenError("No data received"), context: "token_exchange")
|
|
542
|
+
return
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
do {
|
|
546
|
+
let json = try JSONSerialization.jsonObject(with: data) as? [String: Any]
|
|
547
|
+
guard let json = json,
|
|
548
|
+
let accessToken = json["access_token"] as? String,
|
|
549
|
+
let refreshToken = json["refresh_token"] as? String,
|
|
550
|
+
let expiresIn = json["expires_in"] as? TimeInterval else {
|
|
551
|
+
throw SpotifyAuthError.tokenError("Invalid token response format")
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
let expirationDate = Date(timeIntervalSinceNow: expiresIn)
|
|
555
|
+
let sessionData = SpotifySessionData(accessToken: accessToken, refreshToken: refreshToken, expirationDate: expirationDate)
|
|
556
|
+
DispatchQueue.main.async {
|
|
557
|
+
self?.currentSession = sessionData
|
|
558
|
+
self?.module?.onAccessTokenObtained(accessToken)
|
|
559
|
+
}
|
|
560
|
+
} catch {
|
|
561
|
+
self?.handleError(error, context: "token_exchange")
|
|
562
|
+
}
|
|
536
563
|
}
|
|
537
564
|
|
|
538
565
|
task.resume()
|
|
@@ -588,18 +615,24 @@ final class SpotifyAuthAuth: NSObject, SPTSessionManagerDelegate, SpotifyOAuthVi
|
|
|
588
615
|
}
|
|
589
616
|
|
|
590
617
|
private func handleError(_ error: Error, context: String) {
|
|
591
|
-
let spotifyError: SpotifyAuthError
|
|
618
|
+
let spotifyError: SpotifyAuthError
|
|
619
|
+
|
|
620
|
+
if let existingSpotifyError = error as? SpotifyAuthError {
|
|
621
|
+
spotifyError = existingSpotifyError
|
|
622
|
+
} else {
|
|
623
|
+
spotifyError = .authenticationFailed(error.localizedDescription)
|
|
624
|
+
}
|
|
592
625
|
|
|
593
626
|
secureLog("Error in \(context): \(spotifyError.localizedDescription)")
|
|
594
627
|
|
|
595
628
|
switch spotifyError.retryStrategy {
|
|
596
629
|
case .none:
|
|
597
|
-
|
|
598
|
-
|
|
630
|
+
module?.onAuthorizationError(spotifyError.localizedDescription)
|
|
631
|
+
cleanupPreviousSession()
|
|
599
632
|
case .retry(let attempts, let delay):
|
|
600
|
-
|
|
633
|
+
handleRetry(error: spotifyError, context: context, remainingAttempts: attempts, delay: delay)
|
|
601
634
|
case .exponentialBackoff(let maxAttempts, let initialDelay):
|
|
602
|
-
|
|
635
|
+
handleExponentialBackoff(error: spotifyError, context: context, remainingAttempts: maxAttempts, currentDelay: initialDelay)
|
|
603
636
|
}
|
|
604
637
|
}
|
|
605
638
|
|
|
@@ -146,20 +146,115 @@ public class SpotifyAuthModule: Module {
|
|
|
146
146
|
}
|
|
147
147
|
|
|
148
148
|
@objc
|
|
149
|
-
public func onAuthorizationError(_
|
|
150
|
-
let
|
|
151
|
-
|
|
149
|
+
public func onAuthorizationError(_ error: Error) {
|
|
150
|
+
let errorData: [String: Any]
|
|
151
|
+
|
|
152
|
+
if let spotifyError = error as? SpotifyAuthError {
|
|
153
|
+
// Map domain error to a structured format
|
|
154
|
+
errorData = mapSpotifyError(spotifyError)
|
|
155
|
+
} else if let sptError = error as? SPTError {
|
|
156
|
+
// Map Spotify SDK errors
|
|
157
|
+
errorData = mapSPTError(sptError)
|
|
158
|
+
} else {
|
|
159
|
+
// Map unknown errors
|
|
160
|
+
errorData = [
|
|
161
|
+
"type": "unknown_error",
|
|
162
|
+
"message": sanitizeErrorMessage(error.localizedDescription),
|
|
163
|
+
"details": [
|
|
164
|
+
"error_code": "unknown",
|
|
165
|
+
"recoverable": false
|
|
166
|
+
]
|
|
167
|
+
]
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
secureLog("Authorization error: \(errorData["message"] as? String ?? "Unknown error")")
|
|
171
|
+
|
|
152
172
|
let eventData: [String: Any] = [
|
|
153
173
|
"success": false,
|
|
154
|
-
"token": NSNull(),
|
|
155
|
-
"error":
|
|
174
|
+
"token": NSNull(),
|
|
175
|
+
"error": errorData
|
|
156
176
|
]
|
|
157
177
|
sendEvent(SPOTIFY_AUTHORIZATION_EVENT_NAME, eventData)
|
|
158
178
|
}
|
|
159
179
|
|
|
180
|
+
private func mapSpotifyError(_ error: SpotifyAuthError) -> [String: Any] {
|
|
181
|
+
let message = sanitizeErrorMessage(error.localizedDescription)
|
|
182
|
+
var details: [String: Any] = ["recoverable": error.isRecoverable]
|
|
183
|
+
|
|
184
|
+
let (type, errorCode) = classifySpotifyError(error)
|
|
185
|
+
details["error_code"] = errorCode
|
|
186
|
+
|
|
187
|
+
// Add retry strategy information if available
|
|
188
|
+
switch error.retryStrategy {
|
|
189
|
+
case .retry(let attempts, let delay):
|
|
190
|
+
details["retry"] = [
|
|
191
|
+
"type": "fixed",
|
|
192
|
+
"attempts": attempts,
|
|
193
|
+
"delay": delay
|
|
194
|
+
]
|
|
195
|
+
case .exponentialBackoff(let maxAttempts, let initialDelay):
|
|
196
|
+
details["retry"] = [
|
|
197
|
+
"type": "exponential",
|
|
198
|
+
"max_attempts": maxAttempts,
|
|
199
|
+
"initial_delay": initialDelay
|
|
200
|
+
]
|
|
201
|
+
case .none:
|
|
202
|
+
details["retry"] = nil
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return [
|
|
206
|
+
"type": type,
|
|
207
|
+
"message": message,
|
|
208
|
+
"details": details
|
|
209
|
+
]
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
private func mapSPTError(_ error: SPTError) -> [String: Any] {
|
|
213
|
+
let message = sanitizeErrorMessage(error.localizedDescription)
|
|
214
|
+
let details: [String: Any] = [
|
|
215
|
+
"error_code": error.code,
|
|
216
|
+
"recoverable": false
|
|
217
|
+
]
|
|
218
|
+
|
|
219
|
+
let type: String
|
|
220
|
+
switch error.code {
|
|
221
|
+
case .authorizationFailed:
|
|
222
|
+
type = "authorization_error"
|
|
223
|
+
case .renewSessionFailed:
|
|
224
|
+
type = "token_error"
|
|
225
|
+
case .jsonFailed:
|
|
226
|
+
type = "server_error"
|
|
227
|
+
default:
|
|
228
|
+
type = "unknown_error"
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return [
|
|
232
|
+
"type": type,
|
|
233
|
+
"message": message,
|
|
234
|
+
"details": details
|
|
235
|
+
]
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
private func classifySpotifyError(_ error: SpotifyAuthError) -> (type: String, code: String) {
|
|
239
|
+
switch error {
|
|
240
|
+
case .missingConfiguration, .invalidConfiguration:
|
|
241
|
+
return ("configuration_error", "config_invalid")
|
|
242
|
+
case .authenticationFailed:
|
|
243
|
+
return ("authorization_error", "auth_failed")
|
|
244
|
+
case .tokenError:
|
|
245
|
+
return ("token_error", "token_invalid")
|
|
246
|
+
case .sessionError:
|
|
247
|
+
return ("authorization_error", "session_error")
|
|
248
|
+
case .networkError:
|
|
249
|
+
return ("network_error", "network_failed")
|
|
250
|
+
case .recoverable:
|
|
251
|
+
return ("authorization_error", "recoverable_error")
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
160
255
|
func presentWebAuth(_ webAuthView: SpotifyOAuthView) {
|
|
161
256
|
guard let topViewController = UIApplication.shared.currentKeyWindow?.rootViewController?.topMostViewController() else {
|
|
162
|
-
onAuthorizationError("Could not present web authentication")
|
|
257
|
+
onAuthorizationError(SpotifyAuthError.unknownError("Could not present web authentication"))
|
|
163
258
|
return
|
|
164
259
|
}
|
|
165
260
|
|
package/package.json
CHANGED
package/src/SpotifyAuth.types.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { createContext } from "react";
|
|
|
6
6
|
export interface SpotifyAuthEvent {
|
|
7
7
|
success: boolean;
|
|
8
8
|
token: string | null;
|
|
9
|
-
error?:
|
|
9
|
+
error?: SpotifyAuthError;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
/**
|
|
@@ -17,8 +17,44 @@ export interface SpotifyAuthorizationData {
|
|
|
17
17
|
success: boolean;
|
|
18
18
|
/** The access token if authorization was successful, null otherwise */
|
|
19
19
|
token: string | null;
|
|
20
|
-
/** Error
|
|
21
|
-
error?:
|
|
20
|
+
/** Error information if authorization failed */
|
|
21
|
+
error?: SpotifyAuthError;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Possible error types that can occur during Spotify authentication
|
|
26
|
+
*/
|
|
27
|
+
export type SpotifyAuthError = {
|
|
28
|
+
/** The type of error that occurred */
|
|
29
|
+
type:
|
|
30
|
+
| "configuration_error" // Missing or invalid configuration
|
|
31
|
+
| "network_error" // Network-related issues
|
|
32
|
+
| "token_error" // Issues with token exchange/refresh
|
|
33
|
+
| "authorization_error" // User-facing authorization issues
|
|
34
|
+
| "server_error" // Backend server issues
|
|
35
|
+
| "unknown_error"; // Unexpected errors
|
|
36
|
+
/** Human-readable error message */
|
|
37
|
+
message: string;
|
|
38
|
+
/** Additional error details */
|
|
39
|
+
details: {
|
|
40
|
+
/** Specific error code for more granular error handling */
|
|
41
|
+
error_code: string;
|
|
42
|
+
/** Whether the error can be recovered from */
|
|
43
|
+
recoverable: boolean;
|
|
44
|
+
/** Retry strategy information if applicable */
|
|
45
|
+
retry?: {
|
|
46
|
+
/** Type of retry strategy */
|
|
47
|
+
type: "fixed" | "exponential";
|
|
48
|
+
/** For fixed retry: number of attempts */
|
|
49
|
+
attempts?: number;
|
|
50
|
+
/** For fixed retry: delay between attempts in seconds */
|
|
51
|
+
delay?: number;
|
|
52
|
+
/** For exponential backoff: maximum number of attempts */
|
|
53
|
+
max_attempts?: number;
|
|
54
|
+
/** For exponential backoff: initial delay in seconds */
|
|
55
|
+
initial_delay?: number;
|
|
56
|
+
};
|
|
57
|
+
};
|
|
22
58
|
}
|
|
23
59
|
|
|
24
60
|
/**
|
|
@@ -54,7 +90,7 @@ export interface SpotifyAuthContext {
|
|
|
54
90
|
/** Whether authorization is in progress */
|
|
55
91
|
isAuthenticating: boolean;
|
|
56
92
|
/** Last error that occurred during authentication */
|
|
57
|
-
error:
|
|
93
|
+
error: SpotifyAuthError | null;
|
|
58
94
|
}
|
|
59
95
|
|
|
60
96
|
export const SpotifyAuthContextInstance = createContext<SpotifyAuthContext>({
|
package/src/index.tsx
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
SpotifyAuthContext,
|
|
7
7
|
SpotifyAuthContextInstance,
|
|
8
8
|
type AuthorizeConfig,
|
|
9
|
+
type SpotifyAuthError,
|
|
9
10
|
} from "./SpotifyAuth.types";
|
|
10
11
|
import SpotifyAuthModule from "./SpotifyAuthModule";
|
|
11
12
|
|
|
@@ -37,7 +38,7 @@ export function SpotifyAuthProvider({
|
|
|
37
38
|
}: SpotifyAuthProviderProps): JSX.Element {
|
|
38
39
|
const [token, setToken] = useState<string | null>(null);
|
|
39
40
|
const [isAuthenticating, setIsAuthenticating] = useState(false);
|
|
40
|
-
const [error, setError] = useState<
|
|
41
|
+
const [error, setError] = useState<SpotifyAuthError | null>(null);
|
|
41
42
|
|
|
42
43
|
const authorize = useCallback(
|
|
43
44
|
async (config: AuthorizeConfig): Promise<void> => {
|
|
@@ -46,10 +47,21 @@ export function SpotifyAuthProvider({
|
|
|
46
47
|
setError(null);
|
|
47
48
|
await SpotifyAuthModule.authorize(config);
|
|
48
49
|
} catch (err) {
|
|
49
|
-
|
|
50
|
+
// Handle structured errors from the native layer
|
|
51
|
+
if (err && typeof err === 'object' && 'type' in err) {
|
|
52
|
+
setError(err as SpotifyAuthError);
|
|
53
|
+
} else {
|
|
54
|
+
// Create a generic error structure for unknown errors
|
|
55
|
+
setError({
|
|
56
|
+
type: 'unknown_error',
|
|
57
|
+
message: err instanceof Error ? err.message : 'Authorization failed',
|
|
58
|
+
details: {
|
|
59
|
+
error_code: 'unknown',
|
|
60
|
+
recoverable: false
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
}
|
|
50
64
|
throw err;
|
|
51
|
-
} finally {
|
|
52
|
-
setIsAuthenticating(false);
|
|
53
65
|
}
|
|
54
66
|
},
|
|
55
67
|
[],
|
|
@@ -58,8 +70,13 @@ export function SpotifyAuthProvider({
|
|
|
58
70
|
useEffect(() => {
|
|
59
71
|
const subscription = addAuthListener((data) => {
|
|
60
72
|
setToken(data.token);
|
|
73
|
+
setIsAuthenticating(false);
|
|
74
|
+
|
|
61
75
|
if (data.error) {
|
|
62
|
-
console.error(
|
|
76
|
+
console.error('Spotify auth error:', data.error);
|
|
77
|
+
setError(data.error);
|
|
78
|
+
} else {
|
|
79
|
+
setError(null);
|
|
63
80
|
}
|
|
64
81
|
});
|
|
65
82
|
return () => subscription.remove();
|