@superfan-app/spotify-auth 0.1.39 → 0.1.41
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 +7 -5
- package/build/SpotifyAuth.types.d.ts.map +1 -1
- package/build/SpotifyAuth.types.js.map +1 -1
- package/build/SpotifyAuthModule.d.ts +10 -1
- package/build/SpotifyAuthModule.d.ts.map +1 -1
- package/build/SpotifyAuthModule.js.map +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +1 -0
- package/build/index.js.map +1 -1
- package/ios/SpotifyAuthAuth.swift +60 -83
- package/ios/SpotifyAuthModule.swift +1 -16
- package/ios/SpotifyOAuthView.swift +17 -5
- package/package.json +1 -1
- package/plugin/build/index.js +13 -4
- package/plugin/build/types.d.ts +1 -1
- package/plugin/src/index.ts +16 -4
- package/plugin/src/types.ts +7 -6
- package/src/SpotifyAuth.types.ts +27 -5
- package/src/SpotifyAuthModule.ts +12 -2
- package/src/index.tsx +1 -0
- package/ios/TokenManager.swift +0 -51
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Available Spotify authorization scopes.
|
|
3
|
+
* @see https://developer.spotify.com/documentation/general/guides/authorization/scopes/
|
|
4
|
+
*/
|
|
5
|
+
export type SpotifyScopes = 'app-remote-control' | 'playlist-modify-private' | 'playlist-modify-public' | 'playlist-read-collaborative' | 'playlist-read-private' | 'streaming' | 'user-follow-modify' | 'user-follow-read' | 'user-library-modify' | 'user-library-read' | 'user-modify-playback-state' | 'user-read-currently-playing' | 'user-read-email' | 'user-read-playback-position' | 'user-read-playback-state' | 'user-read-private' | 'user-read-recently-played' | 'user-top-read' | 'openid';
|
|
1
6
|
/**
|
|
2
7
|
* Event data structure for Spotify authorization events
|
|
3
8
|
*/
|
|
@@ -47,13 +52,10 @@ export type SpotifyAuthError = {
|
|
|
47
52
|
};
|
|
48
53
|
};
|
|
49
54
|
/**
|
|
50
|
-
* Configuration for the authorization request
|
|
55
|
+
* Configuration for the authorization request.
|
|
56
|
+
* These are runtime options that can be changed between auth attempts.
|
|
51
57
|
*/
|
|
52
58
|
export interface AuthorizeConfig {
|
|
53
|
-
/** Spotify Client ID */
|
|
54
|
-
clientId: string;
|
|
55
|
-
/** OAuth redirect URL */
|
|
56
|
-
redirectUrl: string;
|
|
57
59
|
/** Whether to show the auth dialog */
|
|
58
60
|
showDialog?: boolean;
|
|
59
61
|
/** Campaign identifier for attribution */
|
|
@@ -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,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
|
|
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,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;AAqHtC,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 * 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 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 * 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 * 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"]}
|
|
@@ -1,4 +1,13 @@
|
|
|
1
|
-
|
|
1
|
+
import { NativeModule } from "expo-modules-core";
|
|
2
|
+
import type { AuthorizeConfig, SpotifyAuthEvent } from "./SpotifyAuth.types";
|
|
3
|
+
type SpotifyAuthEvents = {
|
|
4
|
+
onSpotifyAuth(event: SpotifyAuthEvent): void;
|
|
5
|
+
};
|
|
6
|
+
export declare class SpotifyAuthModule extends NativeModule<SpotifyAuthEvents> {
|
|
7
|
+
readonly AuthEventName: string;
|
|
8
|
+
readonly authorize: (config: AuthorizeConfig) => Promise<void>;
|
|
9
|
+
}
|
|
10
|
+
declare const _default: SpotifyAuthModule;
|
|
2
11
|
export default _default;
|
|
3
12
|
export declare const AuthEventName: "onSpotifyAuth";
|
|
4
13
|
//# sourceMappingURL=SpotifyAuthModule.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SpotifyAuthModule.d.ts","sourceRoot":"","sources":["../src/SpotifyAuthModule.ts"],"names":[],"mappings":";
|
|
1
|
+
{"version":3,"file":"SpotifyAuthModule.d.ts","sourceRoot":"","sources":["../src/SpotifyAuthModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAuB,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACtE,OAAO,KAAK,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAE7E,KAAK,iBAAiB,GAAG;IACvB,aAAa,CAAC,KAAK,EAAE,gBAAgB,GAAG,IAAI,CAAC;CAC9C,CAAC;AAEF,MAAM,CAAC,OAAO,OAAO,iBAAkB,SAAQ,YAAY,CAAC,iBAAiB,CAAC;IAC5E,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,SAAS,EAAE,CAAC,MAAM,EAAE,eAAe,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAChE;wBAGoD,iBAAiB;AAAtE,wBAAuE;AAEvE,eAAO,MAAM,aAAa,EAAG,eAAwB,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SpotifyAuthModule.js","sourceRoot":"","sources":["../src/SpotifyAuthModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,
|
|
1
|
+
{"version":3,"file":"SpotifyAuthModule.js","sourceRoot":"","sources":["../src/SpotifyAuthModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAgB,MAAM,mBAAmB,CAAC;AAYtE,yDAAyD;AACzD,eAAe,mBAAmB,CAAC,aAAa,CAAsB,CAAC;AAEvE,MAAM,CAAC,MAAM,aAAa,GAAG,eAAwB,CAAC","sourcesContent":["import { requireNativeModule, NativeModule } from \"expo-modules-core\";\nimport type { AuthorizeConfig, SpotifyAuthEvent } from \"./SpotifyAuth.types\";\n\ntype SpotifyAuthEvents = {\n onSpotifyAuth(event: SpotifyAuthEvent): void;\n};\n\nexport declare class SpotifyAuthModule extends NativeModule<SpotifyAuthEvents> {\n readonly AuthEventName: string;\n readonly authorize: (config: AuthorizeConfig) => Promise<void>;\n}\n\n// This call loads the native module object from the JSI.\nexport default requireNativeModule(\"SpotifyAuth\") as SpotifyAuthModule;\n\nexport const AuthEventName = \"onSpotifyAuth\" as const;\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,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,
|
|
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,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,CA4DxC;AAED,wBAAgB,cAAc,IAAI,kBAAkB,CAMnD"}
|
package/build/index.js
CHANGED
|
@@ -23,6 +23,7 @@ export function SpotifyAuthProvider({ children, }) {
|
|
|
23
23
|
const authorize = useCallback(async (config) => {
|
|
24
24
|
try {
|
|
25
25
|
console.log('[SpotifyAuth] Starting authorization process in provider');
|
|
26
|
+
console.log('[SpotifyAuth] Authorization config:', JSON.stringify(config));
|
|
26
27
|
setIsAuthenticating(true);
|
|
27
28
|
setError(null);
|
|
28
29
|
await SpotifyAuthModule.authorize(config);
|
package/build/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
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 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 [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 console.log('[SpotifyAuth] Starting authorization process in provider');\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:', data.token ? 'Token received' : 'No token');\n setToken(data.token);\n setIsAuthenticating(false);\n\n if (data.error) {\n console.error('[SpotifyAuth] Auth event error:', 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"]}
|
|
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,GAG3B,MAAM,qBAAqB,CAAC;AAC7B,OAAO,iBAAiB,MAAM,qBAAqB,CAAC;AAKpD,kCAAkC;AAClC,MAAM,OAAO,GAAG,IAAI,YAAY,CAAC,iBAAiB,CAAC,CAAC;AAEpD,SAAS,eAAe,CAAC,QAAkD;IACzE,+CAA+C;IAC/C,MAAM,SAAS,GAAG,iBAAiB,CAAC,aAAqC,CAAC;IAC1E,OAAO,OAAO,CAAC,WAAW,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;AAClD,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,SAAS,CAAC,MAAuB;IAC/C,OAAO,CAAC,GAAG,CAAC,gDAAgD,CAAC,CAAC;IAC9D,iBAAiB,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;AACtC,CAAC;AAMD,MAAM,UAAU,mBAAmB,CAAC,EAClC,QAAQ,GACiB;IACzB,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAgB,IAAI,CAAC,CAAC;IACxD,MAAM,CAAC,gBAAgB,EAAE,mBAAmB,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC;IAChE,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAA0B,IAAI,CAAC,CAAC;IAElE,MAAM,SAAS,GAAG,WAAW,CAC3B,KAAK,EAAE,MAAuB,EAAiB,EAAE;QAC/C,IAAI,CAAC;YACH,OAAO,CAAC,GAAG,CAAC,0DAA0D,CAAC,CAAC;YACxE,OAAO,CAAC,GAAG,CAAC,qCAAqC,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC;YAC3E,mBAAmB,CAAC,IAAI,CAAC,CAAC;YAC1B,QAAQ,CAAC,IAAI,CAAC,CAAC;YACf,MAAM,iBAAiB,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;QAC5C,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,OAAO,CAAC,KAAK,CAAC,oCAAoC,EAAE,GAAG,CAAC,CAAC;YACzD,iDAAiD;YACjD,IAAI,GAAG,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,MAAM,IAAI,GAAG,EAAE,CAAC;gBACpD,QAAQ,CAAC,GAAuB,CAAC,CAAC;YACpC,CAAC;iBAAM,CAAC;gBACN,sDAAsD;gBACtD,QAAQ,CAAC;oBACP,IAAI,EAAE,eAAe;oBACrB,OAAO,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,sBAAsB;oBACpE,OAAO,EAAE;wBACP,UAAU,EAAE,SAAS;wBACrB,WAAW,EAAE,KAAK;qBACnB;iBACF,CAAC,CAAC;YACL,CAAC;YACD,MAAM,GAAG,CAAC;QACZ,CAAC;IACH,CAAC,EACD,EAAE,CACH,CAAC;IAEF,SAAS,CAAC,GAAG,EAAE;QACb,OAAO,CAAC,GAAG,CAAC,wCAAwC,CAAC,CAAC;QACtD,MAAM,YAAY,GAAG,eAAe,CAAC,CAAC,IAAI,EAAE,EAAE;YAC5C,OAAO,CAAC,GAAG,CAAC,oCAAoC,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,gBAAgB,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC;YAC9F,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACrB,mBAAmB,CAAC,KAAK,CAAC,CAAC;YAE3B,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;gBACf,OAAO,CAAC,KAAK,CAAC,iCAAiC,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC;gBAC7D,OAAO,CAAC,KAAK,CAAC,qBAAqB,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC;gBACjD,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACvB,CAAC;iBAAM,CAAC;gBACN,QAAQ,CAAC,IAAI,CAAC,CAAC;YACjB,CAAC;QACH,CAAC,CAAC,CAAC;QACH,OAAO,GAAG,EAAE,CAAC,YAAY,CAAC,MAAM,EAAE,CAAC;IACrC,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,OAAO,CACL,CAAC,0BAA0B,CAAC,QAAQ,CAClC,KAAK,CAAC,CAAC,EAAE,WAAW,EAAE,KAAK,EAAE,SAAS,EAAE,gBAAgB,EAAE,KAAK,EAAE,CAAC,CAElE;MAAA,CAAC,QAAQ,CACX;IAAA,EAAE,0BAA0B,CAAC,QAAQ,CAAC,CACvC,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,cAAc;IAC5B,MAAM,OAAO,GAAG,UAAU,CAAC,0BAA0B,CAAC,CAAC;IACvD,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,MAAM,IAAI,KAAK,CAAC,0DAA0D,CAAC,CAAC;IAC9E,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC","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 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 [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 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:', data.token ? 'Token received' : 'No token');\n setToken(data.token);\n setIsAuthenticating(false);\n\n if (data.error) {\n console.error('[SpotifyAuth] Auth event error:', 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"]}
|
|
@@ -125,77 +125,44 @@ final class SpotifyAuthAuth: NSObject, SPTSessionManagerDelegate, SpotifyOAuthVi
|
|
|
125
125
|
|
|
126
126
|
// MARK: - Configuration Accessors
|
|
127
127
|
|
|
128
|
+
private func getInfoPlistValue<T>(_ key: String) throws -> T {
|
|
129
|
+
guard let value = Bundle.main.object(forInfoDictionaryKey: key) as? T else {
|
|
130
|
+
throw SpotifyAuthError.missingConfiguration("Missing \(key) in Info.plist")
|
|
131
|
+
}
|
|
132
|
+
return value
|
|
133
|
+
}
|
|
134
|
+
|
|
128
135
|
private var clientID: String {
|
|
129
136
|
get throws {
|
|
130
|
-
|
|
131
|
-
throw SpotifyAuthError.missingConfiguration("No active configuration")
|
|
132
|
-
}
|
|
133
|
-
guard !config.clientId.isEmpty else {
|
|
134
|
-
throw SpotifyAuthError.missingConfiguration("clientId")
|
|
135
|
-
}
|
|
136
|
-
return config.clientId
|
|
137
|
+
try getInfoPlistValue("SpotifyClientID")
|
|
137
138
|
}
|
|
138
139
|
}
|
|
139
140
|
|
|
140
141
|
private var redirectURL: URL {
|
|
141
142
|
get throws {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
}
|
|
145
|
-
guard let url = URL(string: config.redirectUrl),
|
|
146
|
-
url.scheme != nil,
|
|
147
|
-
url.host != nil else {
|
|
143
|
+
let urlString: String = try getInfoPlistValue("SpotifyRedirectURL")
|
|
144
|
+
guard let url = URL(string: urlString) else {
|
|
148
145
|
throw SpotifyAuthError.invalidConfiguration("Invalid redirect URL format")
|
|
149
146
|
}
|
|
150
147
|
return url
|
|
151
148
|
}
|
|
152
149
|
}
|
|
153
150
|
|
|
154
|
-
private var
|
|
151
|
+
private var tokenSwapURL: String {
|
|
155
152
|
get throws {
|
|
156
|
-
|
|
157
|
-
throw SpotifyAuthError.missingConfiguration("No active configuration")
|
|
158
|
-
}
|
|
159
|
-
guard !config.tokenRefreshURL.isEmpty else {
|
|
160
|
-
throw SpotifyAuthError.missingConfiguration("tokenRefreshURL")
|
|
161
|
-
}
|
|
162
|
-
return config.tokenRefreshURL
|
|
153
|
+
try getInfoPlistValue("SpotifyTokenSwapURL")
|
|
163
154
|
}
|
|
164
155
|
}
|
|
165
156
|
|
|
166
|
-
private var
|
|
157
|
+
private var tokenRefreshURL: String {
|
|
167
158
|
get throws {
|
|
168
|
-
|
|
169
|
-
throw SpotifyAuthError.missingConfiguration("No active configuration")
|
|
170
|
-
}
|
|
171
|
-
guard !config.tokenSwapURL.isEmpty else {
|
|
172
|
-
throw SpotifyAuthError.missingConfiguration("tokenSwapURL")
|
|
173
|
-
}
|
|
174
|
-
return config.tokenSwapURL
|
|
159
|
+
try getInfoPlistValue("SpotifyTokenRefreshURL")
|
|
175
160
|
}
|
|
176
161
|
}
|
|
177
162
|
|
|
178
|
-
private var
|
|
163
|
+
private var scopes: [String] {
|
|
179
164
|
get throws {
|
|
180
|
-
|
|
181
|
-
throw SpotifyAuthError.missingConfiguration("No active configuration")
|
|
182
|
-
}
|
|
183
|
-
guard !config.scopes.isEmpty else {
|
|
184
|
-
throw SpotifyAuthError.missingConfiguration("scopes")
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
var combinedScope: SPTScope = []
|
|
188
|
-
for scopeString in config.scopes {
|
|
189
|
-
if let scope = stringToScope(scopeString: scopeString) {
|
|
190
|
-
combinedScope.insert(scope)
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
if combinedScope.isEmpty {
|
|
195
|
-
throw SpotifyAuthError.invalidConfiguration("No valid scopes provided")
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
return combinedScope
|
|
165
|
+
try getInfoPlistValue("SpotifyScopes")
|
|
199
166
|
}
|
|
200
167
|
}
|
|
201
168
|
|
|
@@ -374,44 +341,53 @@ final class SpotifyAuthAuth: NSObject, SPTSessionManagerDelegate, SpotifyOAuthVi
|
|
|
374
341
|
|
|
375
342
|
public func initAuth(config: AuthorizeConfig) {
|
|
376
343
|
do {
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
guard let sessionManager = self.sessionManager else {
|
|
381
|
-
throw SpotifyAuthError.sessionError("Session manager not initialized")
|
|
382
|
-
}
|
|
383
|
-
let scopes = try self.requestedScopes
|
|
384
|
-
isAuthenticating = true
|
|
385
|
-
|
|
386
|
-
if sessionManager.isSpotifyAppInstalled {
|
|
387
|
-
// Use the native app‑switch flow.
|
|
388
|
-
if config.showDialog {
|
|
389
|
-
sessionManager.alwaysShowAuthorizationDialog = true
|
|
344
|
+
guard let sessionManager = self.sessionManager else {
|
|
345
|
+
throw SpotifyAuthError.sessionError("Session manager not initialized")
|
|
390
346
|
}
|
|
391
|
-
sessionManager.initiateSession(with: scopes, options: .default, campaign: config.campaign)
|
|
392
|
-
} else {
|
|
393
|
-
// Fall back to web‑auth.
|
|
394
|
-
isUsingWebAuth = true
|
|
395
|
-
let clientId = try self.clientID
|
|
396
|
-
let redirectUrl = try self.redirectURL
|
|
397
347
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
348
|
+
// Get scopes from Info.plist and convert to SPTScope
|
|
349
|
+
let scopes = try self.scopes.reduce(into: SPTScope()) { result, scopeString in
|
|
350
|
+
if let scope = stringToScope(scopeString: scopeString) {
|
|
351
|
+
result.insert(scope)
|
|
352
|
+
}
|
|
353
|
+
}
|
|
401
354
|
|
|
402
|
-
|
|
355
|
+
if scopes.isEmpty {
|
|
356
|
+
throw SpotifyAuthError.invalidConfiguration("No valid scopes found in configuration")
|
|
357
|
+
}
|
|
403
358
|
|
|
404
|
-
|
|
405
|
-
clientId: clientId,
|
|
406
|
-
redirectUri: redirectUrl.absoluteString,
|
|
407
|
-
scopes: scopeStrings
|
|
408
|
-
)
|
|
359
|
+
isAuthenticating = true
|
|
409
360
|
|
|
410
|
-
|
|
411
|
-
|
|
361
|
+
if sessionManager.isSpotifyAppInstalled {
|
|
362
|
+
// Use the native app‑switch flow
|
|
363
|
+
if config.showDialog {
|
|
364
|
+
sessionManager.alwaysShowAuthorizationDialog = true
|
|
365
|
+
}
|
|
366
|
+
sessionManager.initiateSession(with: scopes, options: .default, campaign: config.campaign)
|
|
367
|
+
} else {
|
|
368
|
+
// Use web auth as fallback
|
|
369
|
+
let webView = SpotifyOAuthView(appContext: nil)
|
|
370
|
+
webView.delegate = self
|
|
371
|
+
self.webAuthView = webView
|
|
372
|
+
isUsingWebAuth = true
|
|
373
|
+
|
|
374
|
+
// Get configuration from Info.plist
|
|
375
|
+
let clientId = try self.clientID
|
|
376
|
+
let redirectUrl = try self.redirectURL
|
|
377
|
+
let scopeStrings = try self.scopes
|
|
378
|
+
|
|
379
|
+
webView.startOAuthFlow(
|
|
380
|
+
clientId: clientId,
|
|
381
|
+
redirectUri: redirectUrl.absoluteString,
|
|
382
|
+
scopes: scopeStrings,
|
|
383
|
+
showDialog: config.showDialog,
|
|
384
|
+
campaign: config.campaign
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
module?.presentWebAuth(webView)
|
|
388
|
+
}
|
|
412
389
|
} catch {
|
|
413
|
-
|
|
414
|
-
handleError(error, context: "authentication")
|
|
390
|
+
module?.onAuthorizationError(error)
|
|
415
391
|
}
|
|
416
392
|
}
|
|
417
393
|
|
|
@@ -422,9 +398,10 @@ final class SpotifyAuthAuth: NSObject, SPTSessionManagerDelegate, SpotifyOAuthVi
|
|
|
422
398
|
guard let sessionManager = self.sessionManager else {
|
|
423
399
|
throw SpotifyAuthError.sessionError("Session manager not initialized")
|
|
424
400
|
}
|
|
425
|
-
let scopes = try self.
|
|
401
|
+
let scopes = try self.scopes
|
|
402
|
+
let sptScopes = SPTScope(rawValue: scopes.joined(separator: " "))
|
|
426
403
|
isAuthenticating = true
|
|
427
|
-
sessionManager.initiateSession(with:
|
|
404
|
+
sessionManager.initiateSession(with: sptScopes, options: .default, campaign: nil)
|
|
428
405
|
} catch {
|
|
429
406
|
isAuthenticating = false
|
|
430
407
|
handleError(error, context: "authentication_retry")
|
|
@@ -20,13 +20,8 @@ func secureLog(_ message: String, sensitive: Bool = false) {
|
|
|
20
20
|
#endif
|
|
21
21
|
|
|
22
22
|
struct AuthorizeConfig: Record {
|
|
23
|
-
@Field var clientId: String
|
|
24
|
-
@Field var redirectUrl: String
|
|
25
23
|
@Field var showDialog: Bool = false
|
|
26
24
|
@Field var campaign: String?
|
|
27
|
-
@Field var tokenSwapURL: String
|
|
28
|
-
@Field var tokenRefreshURL: String
|
|
29
|
-
@Field var scopes: [String]
|
|
30
25
|
}
|
|
31
26
|
|
|
32
27
|
// Define a private enum for mapping Spotify SDK error codes.
|
|
@@ -66,19 +61,9 @@ public class SpotifyAuthModule: Module {
|
|
|
66
61
|
}
|
|
67
62
|
|
|
68
63
|
AsyncFunction("authorize") { (config: AuthorizeConfig, promise: Promise) in
|
|
69
|
-
secureLog("Authorization requested
|
|
70
|
-
|
|
71
|
-
// Sanitize and validate redirect URL.
|
|
72
|
-
guard let url = URL(string: config.redirectUrl),
|
|
73
|
-
url.scheme != nil,
|
|
74
|
-
url.host != nil else {
|
|
75
|
-
secureLog("Invalid redirect URL format: \(config.redirectUrl)")
|
|
76
|
-
promise.reject(SpotifyAuthError.invalidConfiguration("Invalid redirect URL format"))
|
|
77
|
-
return
|
|
78
|
-
}
|
|
64
|
+
secureLog("Authorization requested")
|
|
79
65
|
|
|
80
66
|
do {
|
|
81
|
-
secureLog("Initializing auth with scopes: \(config.scopes.joined(separator: ","))")
|
|
82
67
|
try self.spotifyAuth.initAuth(config: config)
|
|
83
68
|
promise.resolve()
|
|
84
69
|
} catch {
|
|
@@ -103,7 +103,7 @@ class SpotifyOAuthView: ExpoView {
|
|
|
103
103
|
}
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
-
func startOAuthFlow(clientId: String, redirectUri: String, scopes: [String]) {
|
|
106
|
+
func startOAuthFlow(clientId: String, redirectUri: String, scopes: [String], showDialog: Bool = false, campaign: String? = nil) {
|
|
107
107
|
guard !isAuthenticating else { return }
|
|
108
108
|
isAuthenticating = true
|
|
109
109
|
|
|
@@ -123,7 +123,13 @@ class SpotifyOAuthView: ExpoView {
|
|
|
123
123
|
ofTypes: [WKWebsiteDataTypeCookies, WKWebsiteDataTypeSessionStorage],
|
|
124
124
|
modifiedSince: Date(timeIntervalSince1970: 0)
|
|
125
125
|
) { [weak self] in
|
|
126
|
-
self?.initiateAuthRequest(
|
|
126
|
+
self?.initiateAuthRequest(
|
|
127
|
+
clientId: clientId,
|
|
128
|
+
redirectUri: redirectUri,
|
|
129
|
+
scopes: scopes,
|
|
130
|
+
showDialog: showDialog,
|
|
131
|
+
campaign: campaign
|
|
132
|
+
)
|
|
127
133
|
}
|
|
128
134
|
}
|
|
129
135
|
|
|
@@ -153,22 +159,28 @@ class SpotifyOAuthView: ExpoView {
|
|
|
153
159
|
) { }
|
|
154
160
|
}
|
|
155
161
|
|
|
156
|
-
private func initiateAuthRequest(clientId: String, redirectUri: String, scopes: [String]) {
|
|
162
|
+
private func initiateAuthRequest(clientId: String, redirectUri: String, scopes: [String], showDialog: Bool, campaign: String?) {
|
|
157
163
|
guard var urlComponents = URLComponents(string: "https://accounts.spotify.com/authorize") else {
|
|
158
164
|
isAuthenticating = false
|
|
159
165
|
delegate?.oauthView(self, didFailWithError: SpotifyOAuthError.invalidRedirectURL)
|
|
160
166
|
return
|
|
161
167
|
}
|
|
162
168
|
|
|
163
|
-
|
|
169
|
+
var queryItems = [
|
|
164
170
|
URLQueryItem(name: "client_id", value: clientId),
|
|
165
171
|
URLQueryItem(name: "response_type", value: "code"),
|
|
166
172
|
URLQueryItem(name: "redirect_uri", value: redirectUri),
|
|
167
173
|
URLQueryItem(name: "state", value: state),
|
|
168
174
|
URLQueryItem(name: "scope", value: scopes.joined(separator: " ")),
|
|
169
|
-
URLQueryItem(name: "show_dialog", value: "true")
|
|
175
|
+
URLQueryItem(name: "show_dialog", value: showDialog ? "true" : "false")
|
|
170
176
|
]
|
|
171
177
|
|
|
178
|
+
if let campaign = campaign {
|
|
179
|
+
queryItems.append(URLQueryItem(name: "campaign", value: campaign))
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
urlComponents.queryItems = queryItems
|
|
183
|
+
|
|
172
184
|
guard let url = urlComponents.url else {
|
|
173
185
|
isAuthenticating = false
|
|
174
186
|
delegate?.oauthView(self, didFailWithError: SpotifyOAuthError.invalidRedirectURL)
|
package/package.json
CHANGED
package/plugin/build/index.js
CHANGED
|
@@ -16,6 +16,14 @@ function validateSpotifyConfig(config) {
|
|
|
16
16
|
if (!Array.isArray(config.scopes) || config.scopes.length === 0) {
|
|
17
17
|
throw new Error("At least one scope is required");
|
|
18
18
|
}
|
|
19
|
+
// Validate URL scheme format
|
|
20
|
+
if (!/^[a-z][a-z0-9+.-]*$/i.test(config.scheme)) {
|
|
21
|
+
throw new Error("Invalid URL scheme format. Scheme should start with a letter and contain only letters, numbers, plus, period, or hyphen.");
|
|
22
|
+
}
|
|
23
|
+
// Validate callback path
|
|
24
|
+
if (!/^[a-z0-9\-_\/]+$/i.test(config.callback)) {
|
|
25
|
+
throw new Error("Invalid callback path format. Path should contain only letters, numbers, hyphens, underscores, and forward slashes.");
|
|
26
|
+
}
|
|
19
27
|
}
|
|
20
28
|
function validateScheme(scheme) {
|
|
21
29
|
if (!scheme) {
|
|
@@ -48,13 +56,14 @@ const withSpotifyURLSchemes = (config, props) => {
|
|
|
48
56
|
};
|
|
49
57
|
const withSpotifyConfiguration = (config, props) => {
|
|
50
58
|
return (0, config_plugins_1.withInfoPlist)(config, (config) => {
|
|
59
|
+
// Construct the redirect URL from scheme and callback
|
|
60
|
+
const redirectUrl = `${props.scheme}://${props.callback}`;
|
|
51
61
|
// Add Spotify configuration
|
|
52
62
|
config.modResults.SpotifyClientID = props.clientID;
|
|
53
|
-
config.modResults.
|
|
54
|
-
config.modResults.SpotifyCallback = props.callback;
|
|
63
|
+
config.modResults.SpotifyRedirectURL = redirectUrl;
|
|
55
64
|
config.modResults.SpotifyScopes = props.scopes;
|
|
56
|
-
config.modResults.
|
|
57
|
-
config.modResults.
|
|
65
|
+
config.modResults.SpotifyTokenSwapURL = props.tokenSwapURL;
|
|
66
|
+
config.modResults.SpotifyTokenRefreshURL = props.tokenRefreshURL;
|
|
58
67
|
return config;
|
|
59
68
|
});
|
|
60
69
|
};
|
package/plugin/build/types.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Available Spotify authorization scopes.
|
|
3
3
|
* @see https://developer.spotify.com/documentation/general/guides/authorization/scopes/
|
|
4
4
|
*/
|
|
5
|
-
export type SpotifyScopes = 'app-remote-control' | 'playlist-modify-private' | 'playlist-modify-public' | 'playlist-read-collaborative' | 'playlist-read-private' | 'streaming' | 'user-follow-modify' | 'user-follow-read' | 'user-library-modify' | 'user-library-read' | 'user-modify-playback-state' | 'user-read-currently-playing' | 'user-read-email' | 'user-read-playback-position' | 'user-read-playback-state' | 'user-read-private' | 'user-read-recently-played' | 'user-top-read';
|
|
5
|
+
export type SpotifyScopes = 'app-remote-control' | 'playlist-modify-private' | 'playlist-modify-public' | 'playlist-read-collaborative' | 'playlist-read-private' | 'streaming' | 'user-follow-modify' | 'user-follow-read' | 'user-library-modify' | 'user-library-read' | 'user-modify-playback-state' | 'user-read-currently-playing' | 'user-read-email' | 'user-read-playback-position' | 'user-read-playback-state' | 'user-read-private' | 'user-read-recently-played' | 'user-top-read' | 'openid';
|
|
6
6
|
/**
|
|
7
7
|
* Configuration options for the Spotify OAuth module.
|
|
8
8
|
* This should be provided in your app.config.js or app.json under the "plugins" section.
|
package/plugin/src/index.ts
CHANGED
|
@@ -12,6 +12,16 @@ function validateSpotifyConfig(config: SpotifyConfig) {
|
|
|
12
12
|
if (!Array.isArray(config.scopes) || config.scopes.length === 0) {
|
|
13
13
|
throw new Error("At least one scope is required")
|
|
14
14
|
}
|
|
15
|
+
|
|
16
|
+
// Validate URL scheme format
|
|
17
|
+
if (!/^[a-z][a-z0-9+.-]*$/i.test(config.scheme)) {
|
|
18
|
+
throw new Error("Invalid URL scheme format. Scheme should start with a letter and contain only letters, numbers, plus, period, or hyphen.")
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Validate callback path
|
|
22
|
+
if (!/^[a-z0-9\-_\/]+$/i.test(config.callback)) {
|
|
23
|
+
throw new Error("Invalid callback path format. Path should contain only letters, numbers, hyphens, underscores, and forward slashes.")
|
|
24
|
+
}
|
|
15
25
|
}
|
|
16
26
|
|
|
17
27
|
function validateScheme(scheme: string) {
|
|
@@ -50,13 +60,15 @@ const withSpotifyURLSchemes: ConfigPlugin<SpotifyConfig> = (config, props) => {
|
|
|
50
60
|
|
|
51
61
|
const withSpotifyConfiguration: ConfigPlugin<SpotifyConfig> = (config, props) => {
|
|
52
62
|
return withInfoPlist(config, (config) => {
|
|
63
|
+
// Construct the redirect URL from scheme and callback
|
|
64
|
+
const redirectUrl = `${props.scheme}://${props.callback}`
|
|
65
|
+
|
|
53
66
|
// Add Spotify configuration
|
|
54
67
|
config.modResults.SpotifyClientID = props.clientID;
|
|
55
|
-
config.modResults.
|
|
56
|
-
config.modResults.SpotifyCallback = props.callback;
|
|
68
|
+
config.modResults.SpotifyRedirectURL = redirectUrl;
|
|
57
69
|
config.modResults.SpotifyScopes = props.scopes;
|
|
58
|
-
config.modResults.
|
|
59
|
-
config.modResults.
|
|
70
|
+
config.modResults.SpotifyTokenSwapURL = props.tokenSwapURL;
|
|
71
|
+
config.modResults.SpotifyTokenRefreshURL = props.tokenRefreshURL;
|
|
60
72
|
|
|
61
73
|
return config;
|
|
62
74
|
});
|
package/plugin/src/types.ts
CHANGED
|
@@ -21,6 +21,7 @@ export type SpotifyScopes =
|
|
|
21
21
|
| 'user-read-private'
|
|
22
22
|
| 'user-read-recently-played'
|
|
23
23
|
| 'user-top-read'
|
|
24
|
+
| 'openid'
|
|
24
25
|
|
|
25
26
|
/**
|
|
26
27
|
* Configuration options for the Spotify OAuth module.
|
|
@@ -53,14 +54,14 @@ export interface SpotifyConfig {
|
|
|
53
54
|
* Obtain this from your Spotify Developer Dashboard.
|
|
54
55
|
* @see https://developer.spotify.com/dashboard/
|
|
55
56
|
*/
|
|
56
|
-
clientID: string
|
|
57
|
+
clientID: string;
|
|
57
58
|
|
|
58
59
|
/**
|
|
59
60
|
* The URL scheme to use for OAuth callbacks.
|
|
60
61
|
* This should be unique to your app and match your Spotify app settings.
|
|
61
62
|
* @example "my-spotify-app"
|
|
62
63
|
*/
|
|
63
|
-
scheme: string
|
|
64
|
+
scheme: string;
|
|
64
65
|
|
|
65
66
|
/**
|
|
66
67
|
* The callback path for OAuth redirects.
|
|
@@ -68,7 +69,7 @@ export interface SpotifyConfig {
|
|
|
68
69
|
* Full URI will be: `{scheme}://{callback}`
|
|
69
70
|
* @example "callback"
|
|
70
71
|
*/
|
|
71
|
-
callback: string
|
|
72
|
+
callback: string;
|
|
72
73
|
|
|
73
74
|
/**
|
|
74
75
|
* URL for token swap endpoint.
|
|
@@ -76,7 +77,7 @@ export interface SpotifyConfig {
|
|
|
76
77
|
* the code-for-token exchange with Spotify.
|
|
77
78
|
* @example "https://your-backend.com/spotify/swap"
|
|
78
79
|
*/
|
|
79
|
-
tokenSwapURL: string
|
|
80
|
+
tokenSwapURL: string;
|
|
80
81
|
|
|
81
82
|
/**
|
|
82
83
|
* URL for token refresh endpoint.
|
|
@@ -84,7 +85,7 @@ export interface SpotifyConfig {
|
|
|
84
85
|
* refreshing expired access tokens.
|
|
85
86
|
* @example "https://your-backend.com/spotify/refresh"
|
|
86
87
|
*/
|
|
87
|
-
tokenRefreshURL: string
|
|
88
|
+
tokenRefreshURL: string;
|
|
88
89
|
|
|
89
90
|
/**
|
|
90
91
|
* Array of Spotify authorization scopes.
|
|
@@ -92,5 +93,5 @@ export interface SpotifyConfig {
|
|
|
92
93
|
* @see SpotifyScopes for available options
|
|
93
94
|
* @example ["user-read-email", "streaming"]
|
|
94
95
|
*/
|
|
95
|
-
scopes: SpotifyScopes[]
|
|
96
|
+
scopes: SpotifyScopes[];
|
|
96
97
|
}
|
package/src/SpotifyAuth.types.ts
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
import { createContext } from "react";
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Available Spotify authorization scopes.
|
|
5
|
+
* @see https://developer.spotify.com/documentation/general/guides/authorization/scopes/
|
|
6
|
+
*/
|
|
7
|
+
export type SpotifyScopes =
|
|
8
|
+
| 'app-remote-control'
|
|
9
|
+
| 'playlist-modify-private'
|
|
10
|
+
| 'playlist-modify-public'
|
|
11
|
+
| 'playlist-read-collaborative'
|
|
12
|
+
| 'playlist-read-private'
|
|
13
|
+
| 'streaming'
|
|
14
|
+
| 'user-follow-modify'
|
|
15
|
+
| 'user-follow-read'
|
|
16
|
+
| 'user-library-modify'
|
|
17
|
+
| 'user-library-read'
|
|
18
|
+
| 'user-modify-playback-state'
|
|
19
|
+
| 'user-read-currently-playing'
|
|
20
|
+
| 'user-read-email'
|
|
21
|
+
| 'user-read-playback-position'
|
|
22
|
+
| 'user-read-playback-state'
|
|
23
|
+
| 'user-read-private'
|
|
24
|
+
| 'user-read-recently-played'
|
|
25
|
+
| 'user-top-read'
|
|
26
|
+
| 'openid';
|
|
27
|
+
|
|
3
28
|
/**
|
|
4
29
|
* Event data structure for Spotify authorization events
|
|
5
30
|
*/
|
|
@@ -58,13 +83,10 @@ export type SpotifyAuthError = {
|
|
|
58
83
|
}
|
|
59
84
|
|
|
60
85
|
/**
|
|
61
|
-
* Configuration for the authorization request
|
|
86
|
+
* Configuration for the authorization request.
|
|
87
|
+
* These are runtime options that can be changed between auth attempts.
|
|
62
88
|
*/
|
|
63
89
|
export interface AuthorizeConfig {
|
|
64
|
-
/** Spotify Client ID */
|
|
65
|
-
clientId: string;
|
|
66
|
-
/** OAuth redirect URL */
|
|
67
|
-
redirectUrl: string;
|
|
68
90
|
/** Whether to show the auth dialog */
|
|
69
91
|
showDialog?: boolean;
|
|
70
92
|
/** Campaign identifier for attribution */
|
package/src/SpotifyAuthModule.ts
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
|
-
import { requireNativeModule } from "expo-modules-core";
|
|
1
|
+
import { requireNativeModule, NativeModule } from "expo-modules-core";
|
|
2
|
+
import type { AuthorizeConfig, SpotifyAuthEvent } from "./SpotifyAuth.types";
|
|
3
|
+
|
|
4
|
+
type SpotifyAuthEvents = {
|
|
5
|
+
onSpotifyAuth(event: SpotifyAuthEvent): void;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export declare class SpotifyAuthModule extends NativeModule<SpotifyAuthEvents> {
|
|
9
|
+
readonly AuthEventName: string;
|
|
10
|
+
readonly authorize: (config: AuthorizeConfig) => Promise<void>;
|
|
11
|
+
}
|
|
2
12
|
|
|
3
13
|
// This call loads the native module object from the JSI.
|
|
4
|
-
export default requireNativeModule("SpotifyAuth");
|
|
14
|
+
export default requireNativeModule("SpotifyAuth") as SpotifyAuthModule;
|
|
5
15
|
|
|
6
16
|
export const AuthEventName = "onSpotifyAuth" as const;
|
package/src/index.tsx
CHANGED
|
@@ -45,6 +45,7 @@ export function SpotifyAuthProvider({
|
|
|
45
45
|
async (config: AuthorizeConfig): Promise<void> => {
|
|
46
46
|
try {
|
|
47
47
|
console.log('[SpotifyAuth] Starting authorization process in provider');
|
|
48
|
+
console.log('[SpotifyAuth] Authorization config:', JSON.stringify(config));
|
|
48
49
|
setIsAuthenticating(true);
|
|
49
50
|
setError(null);
|
|
50
51
|
await SpotifyAuthModule.authorize(config);
|
package/ios/TokenManager.swift
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import Foundation
|
|
2
|
-
|
|
3
|
-
class TokenManager {
|
|
4
|
-
static let shared = TokenManager()
|
|
5
|
-
private let tokenKey = "com.yourappname.spotifyToken"
|
|
6
|
-
|
|
7
|
-
private init() {}
|
|
8
|
-
|
|
9
|
-
func saveToken(_ token: String) -> Bool {
|
|
10
|
-
guard let data = token.data(using: .utf8) else { return false }
|
|
11
|
-
|
|
12
|
-
let query: [String: Any] = [
|
|
13
|
-
kSecClass as String: kSecClassGenericPassword,
|
|
14
|
-
kSecAttrAccount as String: tokenKey,
|
|
15
|
-
kSecValueData as String: data,
|
|
16
|
-
]
|
|
17
|
-
|
|
18
|
-
SecItemDelete(query as CFDictionary)
|
|
19
|
-
let status = SecItemAdd(query as CFDictionary, nil)
|
|
20
|
-
return status == errSecSuccess
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
func getToken() -> String? {
|
|
24
|
-
let query: [String: Any] = [
|
|
25
|
-
kSecClass as String: kSecClassGenericPassword,
|
|
26
|
-
kSecAttrAccount as String: tokenKey,
|
|
27
|
-
kSecReturnData as String: kCFBooleanTrue!,
|
|
28
|
-
kSecMatchLimit as String: kSecMatchLimitOne,
|
|
29
|
-
]
|
|
30
|
-
|
|
31
|
-
var dataTypeRef: AnyObject?
|
|
32
|
-
let status: OSStatus = SecItemCopyMatching(query as CFDictionary, &dataTypeRef)
|
|
33
|
-
|
|
34
|
-
if status == errSecSuccess {
|
|
35
|
-
if let data = dataTypeRef as? Data {
|
|
36
|
-
return String(data: data, encoding: .utf8)
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
return nil
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
func deleteToken() -> Bool {
|
|
43
|
-
let query: [String: Any] = [
|
|
44
|
-
kSecClass as String: kSecClassGenericPassword,
|
|
45
|
-
kSecAttrAccount as String: tokenKey,
|
|
46
|
-
]
|
|
47
|
-
|
|
48
|
-
let status = SecItemDelete(query as CFDictionary)
|
|
49
|
-
return status == errSecSuccess
|
|
50
|
-
}
|
|
51
|
-
}
|