@wwdrew/expo-spotify-sdk 0.5.0 → 0.7.0

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.
Files changed (60) hide show
  1. package/LICENSE +21 -3
  2. package/README.md +346 -96
  3. package/android/build.gradle +28 -22
  4. package/android/consumer-rules.pro +8 -0
  5. package/android/src/main/AndroidManifest.xml +3 -0
  6. package/android/src/main/java/expo/modules/spotifysdk/ExpoSpotifySDKModule.kt +133 -147
  7. package/android/src/main/java/expo/modules/spotifysdk/SpotifyAuthCoordinator.kt +36 -0
  8. package/android/src/main/java/expo/modules/spotifysdk/SpotifyAuthorizationContract.kt +46 -0
  9. package/android/src/main/java/expo/modules/spotifysdk/SpotifyConfig.kt +84 -0
  10. package/android/src/main/java/expo/modules/spotifysdk/SpotifyErrors.kt +34 -0
  11. package/android/src/main/java/expo/modules/spotifysdk/SpotifyTokenSwapClient.kt +154 -0
  12. package/build/ExpoSpotifySDK.types.d.ts +84 -1
  13. package/build/ExpoSpotifySDK.types.d.ts.map +1 -1
  14. package/build/ExpoSpotifySDK.types.js +12 -1
  15. package/build/ExpoSpotifySDK.types.js.map +1 -1
  16. package/build/ExpoSpotifySDKModule.web.d.ts +7 -3
  17. package/build/ExpoSpotifySDKModule.web.d.ts.map +1 -1
  18. package/build/ExpoSpotifySDKModule.web.js +17 -7
  19. package/build/ExpoSpotifySDKModule.web.js.map +1 -1
  20. package/build/index.d.ts +39 -2
  21. package/build/index.d.ts.map +1 -1
  22. package/build/index.js +117 -4
  23. package/build/index.js.map +1 -1
  24. package/expo-module.config.json +1 -1
  25. package/ios/ExpoSpotifyAppDelegate.swift +14 -0
  26. package/ios/ExpoSpotifyConfiguration.swift +33 -15
  27. package/ios/ExpoSpotifySDK.podspec +2 -4
  28. package/ios/ExpoSpotifySDKModule.swift +98 -26
  29. package/ios/SPTScopeSerializer.swift +40 -39
  30. package/ios/SpotifyAuthCoordinator.swift +186 -0
  31. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTConfiguration.h +1 -1
  32. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTError.h +6 -7
  33. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTScope.h +43 -39
  34. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTSessionManager.h +8 -4
  35. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SpotifyiOS.h +1 -1
  36. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Info.plist +0 -0
  37. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/SpotifyiOS +0 -0
  38. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTConfiguration.h +1 -1
  39. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTError.h +6 -7
  40. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTScope.h +43 -39
  41. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTSessionManager.h +8 -4
  42. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SpotifyiOS.h +1 -1
  43. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Info.plist +0 -0
  44. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/SpotifyiOS +0 -0
  45. package/ios/SpotifyTokenRefreshClient.swift +118 -0
  46. package/package.json +12 -11
  47. package/plugin/build/android/withSpotifyAndroidAppBuildGradle.js +15 -10
  48. package/plugin/build/ios/withSpotifyConfigValues.js +12 -7
  49. package/plugin/build/ios/withSpotifyURLScheme.d.ts +1 -1
  50. package/plugin/build/ios/withSpotifyURLScheme.js +9 -8
  51. package/plugin/build/types.d.ts +35 -0
  52. package/plugin/build/types.js +0 -19
  53. package/ios/ExpoSpotifyAuthDelegate.swift +0 -13
  54. package/ios/ExpoSpotifySessionManager+SessionManagerDelegate.swift +0 -16
  55. package/ios/ExpoSpotifySessionManager.swift +0 -90
  56. package/ios/SpotifySDK/SpotifyiOS.xcframework/_CodeSignature/CodeDirectory +0 -0
  57. package/ios/SpotifySDK/SpotifyiOS.xcframework/_CodeSignature/CodeRequirements +0 -0
  58. package/ios/SpotifySDK/SpotifyiOS.xcframework/_CodeSignature/CodeRequirements-1 +0 -0
  59. package/ios/SpotifySDK/SpotifyiOS.xcframework/_CodeSignature/CodeResources +0 -1118
  60. package/ios/SpotifySDK/SpotifyiOS.xcframework/_CodeSignature/CodeSignature +0 -0
@@ -1,13 +1,96 @@
1
+ /**
2
+ * Result of a successful Spotify authentication.
3
+ *
4
+ * The shape is identical across iOS and Android. On the Android implicit
5
+ * (TOKEN) flow `refreshToken` is `null` and `scopes` reflects what was
6
+ * *requested*, not granted — see the README's "Android implicit flow is not
7
+ * recommended" section.
8
+ */
1
9
  export interface SpotifySession {
10
+ /** OAuth access token. */
2
11
  accessToken: string;
3
- refreshToken: string;
12
+ /**
13
+ * OAuth refresh token. `null` on Android when no `tokenSwapURL` is provided
14
+ * (the Spotify Android SDK does not expose a refresh token for implicit
15
+ * grants — see the README).
16
+ */
17
+ refreshToken: string | null;
18
+ /** Expiration timestamp as Unix epoch milliseconds. */
4
19
  expirationDate: number;
20
+ /** Scopes the access token was granted (or requested, on Android implicit). */
5
21
  scopes: SpotifyScope[];
6
22
  }
23
+ /**
24
+ * Configuration accepted by `refreshSessionAsync`.
25
+ */
26
+ export interface SpotifyRefreshConfig {
27
+ /** The refresh token from a previous `authenticateAsync` call. */
28
+ refreshToken: string;
29
+ /** URL of your token refresh server endpoint. */
30
+ tokenRefreshURL: string;
31
+ }
32
+ /**
33
+ * Configuration accepted by `authenticateAsync`.
34
+ */
7
35
  export interface SpotifyConfig {
36
+ /** OAuth scopes to request. Must contain at least one entry. */
8
37
  scopes: SpotifyScope[];
38
+ /**
39
+ * If supplied, requests an authorization code rather than an implicit
40
+ * token, then POSTs the code to this URL to exchange it for tokens.
41
+ * **Required on Android** to receive a usable `refreshToken`.
42
+ */
9
43
  tokenSwapURL?: string;
44
+ /**
45
+ * Used by the iOS SDK to refresh access tokens automatically, and by
46
+ * `refreshSessionAsync` on both platforms.
47
+ */
10
48
  tokenRefreshURL?: string;
49
+ /**
50
+ * If `true`, forces Spotify to show the authorization dialog even when
51
+ * the user already has an active session. Defaults to `false`.
52
+ *
53
+ * Maps to `SPTSessionManager.alwaysShowAuthorizationDialog` on iOS and
54
+ * `AuthorizationRequest.Builder.setShowDialog(true)` on Android.
55
+ */
56
+ showDialog?: boolean;
11
57
  }
58
+ /**
59
+ * Spotify OAuth scope identifiers that are valid through the iOS, Android
60
+ * and Web auth flows. See https://developer.spotify.com/documentation/web-api/concepts/scopes
61
+ */
12
62
  export type SpotifyScope = "ugc-image-upload" | "user-read-playback-state" | "user-modify-playback-state" | "user-read-currently-playing" | "app-remote-control" | "streaming" | "playlist-read-private" | "playlist-read-collaborative" | "playlist-modify-private" | "playlist-modify-public" | "user-follow-modify" | "user-follow-read" | "user-top-read" | "user-read-recently-played" | "user-library-modify" | "user-library-read" | "user-read-email" | "user-read-private";
63
+ /**
64
+ * JS-side error code constants thrown via `Promise.reject(new Error(...))`
65
+ * by the native modules.
66
+ */
67
+ export type SpotifyErrorCode = "USER_CANCELLED" | "AUTH_IN_PROGRESS" | "INVALID_CONFIG" | "NETWORK_ERROR" | "TOKEN_SWAP_FAILED" | "TOKEN_SWAP_PARSE_ERROR" | "SPOTIFY_NOT_INSTALLED" | "AUTH_ERROR" | "UNKNOWN";
68
+ /**
69
+ * Payload delivered to `addSessionChangeListener` subscribers.
70
+ *
71
+ * - `didInitiate` — a new session was created by `authenticateAsync`
72
+ * - `didRenew` — an existing session was refreshed by `refreshSessionAsync`
73
+ * - `didFail` — an auth or refresh attempt failed
74
+ */
75
+ export type SpotifySessionChangeEvent = {
76
+ type: "didInitiate";
77
+ session: SpotifySession;
78
+ } | {
79
+ type: "didRenew";
80
+ session: SpotifySession;
81
+ } | {
82
+ type: "didFail";
83
+ error: {
84
+ code: SpotifyErrorCode;
85
+ message: string;
86
+ };
87
+ };
88
+ /**
89
+ * Error subclass thrown by `authenticateAsync` and `refreshSessionAsync`
90
+ * carrying a structured `code` field for branching.
91
+ */
92
+ export declare class SpotifyError extends Error {
93
+ readonly code: SpotifyErrorCode;
94
+ constructor(code: SpotifyErrorCode, message: string);
95
+ }
13
96
  //# sourceMappingURL=ExpoSpotifySDK.types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ExpoSpotifySDK.types.d.ts","sourceRoot":"","sources":["../src/ExpoSpotifySDK.types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,cAAc;IAC7B,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,MAAM,CAAC;IACvB,MAAM,EAAE,YAAY,EAAE,CAAC;CACxB;AAED,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,YAAY,EAAE,CAAC;IACvB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAMD,MAAM,MAAM,YAAY,GACpB,kBAAkB,GAClB,0BAA0B,GAC1B,4BAA4B,GAC5B,6BAA6B,GAC7B,oBAAoB,GACpB,WAAW,GACX,uBAAuB,GACvB,6BAA6B,GAC7B,yBAAyB,GACzB,wBAAwB,GACxB,oBAAoB,GACpB,kBAAkB,GAClB,eAAe,GACf,2BAA2B,GAC3B,qBAAqB,GACrB,mBAAmB,GACnB,iBAAiB,GACjB,mBAAmB,CAAC"}
1
+ {"version":3,"file":"ExpoSpotifySDK.types.d.ts","sourceRoot":"","sources":["../src/ExpoSpotifySDK.types.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,MAAM,WAAW,cAAc;IAC7B,0BAA0B;IAC1B,WAAW,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,uDAAuD;IACvD,cAAc,EAAE,MAAM,CAAC;IACvB,+EAA+E;IAC/E,MAAM,EAAE,YAAY,EAAE,CAAC;CACxB;AAED;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,kEAAkE;IAClE,YAAY,EAAE,MAAM,CAAC;IACrB,iDAAiD;IACjD,eAAe,EAAE,MAAM,CAAC;CACzB;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,gEAAgE;IAChE,MAAM,EAAE,YAAY,EAAE,CAAC;IACvB;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED;;;GAGG;AACH,MAAM,MAAM,YAAY,GACpB,kBAAkB,GAClB,0BAA0B,GAC1B,4BAA4B,GAC5B,6BAA6B,GAC7B,oBAAoB,GACpB,WAAW,GACX,uBAAuB,GACvB,6BAA6B,GAC7B,yBAAyB,GACzB,wBAAwB,GACxB,oBAAoB,GACpB,kBAAkB,GAClB,eAAe,GACf,2BAA2B,GAC3B,qBAAqB,GACrB,mBAAmB,GACnB,iBAAiB,GACjB,mBAAmB,CAAC;AAExB;;;GAGG;AACH,MAAM,MAAM,gBAAgB,GACxB,gBAAgB,GAChB,kBAAkB,GAClB,gBAAgB,GAChB,eAAe,GACf,mBAAmB,GACnB,wBAAwB,GACxB,uBAAuB,GACvB,YAAY,GACZ,SAAS,CAAC;AAEd;;;;;;GAMG;AACH,MAAM,MAAM,yBAAyB,GACjC;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,OAAO,EAAE,cAAc,CAAA;CAAE,GAChD;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,OAAO,EAAE,cAAc,CAAA;CAAE,GAC7C;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,KAAK,EAAE;QAAE,IAAI,EAAE,gBAAgB,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,CAAC;AAE5E;;;GAGG;AACH,qBAAa,YAAa,SAAQ,KAAK;IACrC,SAAgB,IAAI,EAAE,gBAAgB,CAAC;gBAE3B,IAAI,EAAE,gBAAgB,EAAE,OAAO,EAAE,MAAM;CAKpD"}
@@ -1,2 +1,13 @@
1
- export {};
1
+ /**
2
+ * Error subclass thrown by `authenticateAsync` and `refreshSessionAsync`
3
+ * carrying a structured `code` field for branching.
4
+ */
5
+ export class SpotifyError extends Error {
6
+ code;
7
+ constructor(code, message) {
8
+ super(message);
9
+ this.name = "SpotifyError";
10
+ this.code = code;
11
+ }
12
+ }
2
13
  //# sourceMappingURL=ExpoSpotifySDK.types.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"ExpoSpotifySDK.types.js","sourceRoot":"","sources":["../src/ExpoSpotifySDK.types.ts"],"names":[],"mappings":"","sourcesContent":["export interface SpotifySession {\n accessToken: string;\n refreshToken: string;\n expirationDate: number;\n scopes: SpotifyScope[];\n}\n\nexport interface SpotifyConfig {\n scopes: SpotifyScope[];\n tokenSwapURL?: string;\n tokenRefreshURL?: string;\n}\n\n// export interface AndroidSpotifyConfig extends SpotifyConfig {\n// responseType: \"token\" | \"code\";\n// }\n\nexport type SpotifyScope =\n | \"ugc-image-upload\"\n | \"user-read-playback-state\"\n | \"user-modify-playback-state\"\n | \"user-read-currently-playing\"\n | \"app-remote-control\"\n | \"streaming\"\n | \"playlist-read-private\"\n | \"playlist-read-collaborative\"\n | \"playlist-modify-private\"\n | \"playlist-modify-public\"\n | \"user-follow-modify\"\n | \"user-follow-read\"\n | \"user-top-read\"\n | \"user-read-recently-played\"\n | \"user-library-modify\"\n | \"user-library-read\"\n | \"user-read-email\"\n | \"user-read-private\";\n"]}
1
+ {"version":3,"file":"ExpoSpotifySDK.types.js","sourceRoot":"","sources":["../src/ExpoSpotifySDK.types.ts"],"names":[],"mappings":"AA+GA;;;GAGG;AACH,MAAM,OAAO,YAAa,SAAQ,KAAK;IACrB,IAAI,CAAmB;IAEvC,YAAY,IAAsB,EAAE,OAAe;QACjD,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,cAAc,CAAC;QAC3B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IACnB,CAAC;CACF","sourcesContent":["/**\n * Result of a successful Spotify authentication.\n *\n * The shape is identical across iOS and Android. On the Android implicit\n * (TOKEN) flow `refreshToken` is `null` and `scopes` reflects what was\n * *requested*, not granted — see the README's \"Android implicit flow is not\n * recommended\" section.\n */\nexport interface SpotifySession {\n /** OAuth access token. */\n accessToken: string;\n /**\n * OAuth refresh token. `null` on Android when no `tokenSwapURL` is provided\n * (the Spotify Android SDK does not expose a refresh token for implicit\n * grants — see the README).\n */\n refreshToken: string | null;\n /** Expiration timestamp as Unix epoch milliseconds. */\n expirationDate: number;\n /** Scopes the access token was granted (or requested, on Android implicit). */\n scopes: SpotifyScope[];\n}\n\n/**\n * Configuration accepted by `refreshSessionAsync`.\n */\nexport interface SpotifyRefreshConfig {\n /** The refresh token from a previous `authenticateAsync` call. */\n refreshToken: string;\n /** URL of your token refresh server endpoint. */\n tokenRefreshURL: string;\n}\n\n/**\n * Configuration accepted by `authenticateAsync`.\n */\nexport interface SpotifyConfig {\n /** OAuth scopes to request. Must contain at least one entry. */\n scopes: SpotifyScope[];\n /**\n * If supplied, requests an authorization code rather than an implicit\n * token, then POSTs the code to this URL to exchange it for tokens.\n * **Required on Android** to receive a usable `refreshToken`.\n */\n tokenSwapURL?: string;\n /**\n * Used by the iOS SDK to refresh access tokens automatically, and by\n * `refreshSessionAsync` on both platforms.\n */\n tokenRefreshURL?: string;\n /**\n * If `true`, forces Spotify to show the authorization dialog even when\n * the user already has an active session. Defaults to `false`.\n *\n * Maps to `SPTSessionManager.alwaysShowAuthorizationDialog` on iOS and\n * `AuthorizationRequest.Builder.setShowDialog(true)` on Android.\n */\n showDialog?: boolean;\n}\n\n/**\n * Spotify OAuth scope identifiers that are valid through the iOS, Android\n * and Web auth flows. See https://developer.spotify.com/documentation/web-api/concepts/scopes\n */\nexport type SpotifyScope =\n | \"ugc-image-upload\"\n | \"user-read-playback-state\"\n | \"user-modify-playback-state\"\n | \"user-read-currently-playing\"\n | \"app-remote-control\"\n | \"streaming\"\n | \"playlist-read-private\"\n | \"playlist-read-collaborative\"\n | \"playlist-modify-private\"\n | \"playlist-modify-public\"\n | \"user-follow-modify\"\n | \"user-follow-read\"\n | \"user-top-read\"\n | \"user-read-recently-played\"\n | \"user-library-modify\"\n | \"user-library-read\"\n | \"user-read-email\"\n | \"user-read-private\";\n\n/**\n * JS-side error code constants thrown via `Promise.reject(new Error(...))`\n * by the native modules.\n */\nexport type SpotifyErrorCode =\n | \"USER_CANCELLED\"\n | \"AUTH_IN_PROGRESS\"\n | \"INVALID_CONFIG\"\n | \"NETWORK_ERROR\"\n | \"TOKEN_SWAP_FAILED\"\n | \"TOKEN_SWAP_PARSE_ERROR\"\n | \"SPOTIFY_NOT_INSTALLED\"\n | \"AUTH_ERROR\"\n | \"UNKNOWN\";\n\n/**\n * Payload delivered to `addSessionChangeListener` subscribers.\n *\n * - `didInitiate` — a new session was created by `authenticateAsync`\n * - `didRenew` — an existing session was refreshed by `refreshSessionAsync`\n * - `didFail` — an auth or refresh attempt failed\n */\nexport type SpotifySessionChangeEvent =\n | { type: \"didInitiate\"; session: SpotifySession }\n | { type: \"didRenew\"; session: SpotifySession }\n | { type: \"didFail\"; error: { code: SpotifyErrorCode; message: string } };\n\n/**\n * Error subclass thrown by `authenticateAsync` and `refreshSessionAsync`\n * carrying a structured `code` field for branching.\n */\nexport class SpotifyError extends Error {\n public readonly code: SpotifyErrorCode;\n\n constructor(code: SpotifyErrorCode, message: string) {\n super(message);\n this.name = \"SpotifyError\";\n this.code = code;\n }\n}\n"]}
@@ -1,7 +1,11 @@
1
1
  declare const _default: {
2
- PI: number;
3
- setValueAsync(value: string): Promise<void>;
4
- hello(): string;
2
+ isAvailable(): boolean;
3
+ authenticateAsync(): Promise<never>;
4
+ refreshSessionAsync(): Promise<never>;
5
+ addListener(): {
6
+ remove(): void;
7
+ };
8
+ removeListeners(): void;
5
9
  };
6
10
  export default _default;
7
11
  //# sourceMappingURL=ExpoSpotifySDKModule.web.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ExpoSpotifySDKModule.web.d.ts","sourceRoot":"","sources":["../src/ExpoSpotifySDKModule.web.ts"],"names":[],"mappings":";;yBAM6B,MAAM,GAAG,QAAQ,IAAI,CAAC;;;AAFnD,wBAQE"}
1
+ {"version":3,"file":"ExpoSpotifySDKModule.web.d.ts","sourceRoot":"","sources":["../src/ExpoSpotifySDKModule.web.ts"],"names":[],"mappings":";mBAUiB,OAAO;yBAGD,OAAO,CAAC,KAAK,CAAC;2BAGZ,OAAO,CAAC,KAAK,CAAC;mBAGtB;QAAE,MAAM,IAAI,IAAI,CAAA;KAAE;uBAGd,IAAI;;AAbzB,wBAgBE"}
@@ -1,12 +1,22 @@
1
- import { EventEmitter } from "expo-modules-core";
2
- const emitter = new EventEmitter({});
1
+ import { SpotifyError } from "./ExpoSpotifySDK.types";
2
+ function notImplementedReject(method) {
3
+ return new SpotifyError("INVALID_CONFIG", `expo-spotify-sdk: ${method} is not implemented on web yet (planned for v0.7.0).`);
4
+ }
3
5
  export default {
4
- PI: Math.PI,
5
- async setValueAsync(value) {
6
- emitter.emit("onChange", { value });
6
+ isAvailable() {
7
+ return false;
7
8
  },
8
- hello() {
9
- return "Hello world! 👋";
9
+ authenticateAsync() {
10
+ return Promise.reject(notImplementedReject("authenticateAsync"));
11
+ },
12
+ refreshSessionAsync() {
13
+ return Promise.reject(notImplementedReject("refreshSessionAsync"));
14
+ },
15
+ addListener() {
16
+ return { remove() { } };
17
+ },
18
+ removeListeners() {
19
+ /* no-op */
10
20
  },
11
21
  };
12
22
  //# sourceMappingURL=ExpoSpotifySDKModule.web.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"ExpoSpotifySDKModule.web.js","sourceRoot":"","sources":["../src/ExpoSpotifySDKModule.web.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAEjD,MAAM,OAAO,GAAG,IAAI,YAAY,CAAC,EAAS,CAAC,CAAC;AAE5C,eAAe;IACb,EAAE,EAAE,IAAI,CAAC,EAAE;IACX,KAAK,CAAC,aAAa,CAAC,KAAa;QAC/B,OAAO,CAAC,IAAI,CAAC,UAAU,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC;IACtC,CAAC;IACD,KAAK;QACH,OAAO,iBAAiB,CAAC;IAC3B,CAAC;CACF,CAAC","sourcesContent":["import { EventEmitter } from \"expo-modules-core\";\n\nconst emitter = new EventEmitter({} as any);\n\nexport default {\n PI: Math.PI,\n async setValueAsync(value: string): Promise<void> {\n emitter.emit(\"onChange\", { value });\n },\n hello() {\n return \"Hello world! 👋\";\n },\n};\n"]}
1
+ {"version":3,"file":"ExpoSpotifySDKModule.web.js","sourceRoot":"","sources":["../src/ExpoSpotifySDKModule.web.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAEtD,SAAS,oBAAoB,CAAC,MAAc;IAC1C,OAAO,IAAI,YAAY,CACrB,gBAAgB,EAChB,qBAAqB,MAAM,sDAAsD,CAClF,CAAC;AACJ,CAAC;AAED,eAAe;IACb,WAAW;QACT,OAAO,KAAK,CAAC;IACf,CAAC;IACD,iBAAiB;QACf,OAAO,OAAO,CAAC,MAAM,CAAC,oBAAoB,CAAC,mBAAmB,CAAC,CAAC,CAAC;IACnE,CAAC;IACD,mBAAmB;QACjB,OAAO,OAAO,CAAC,MAAM,CAAC,oBAAoB,CAAC,qBAAqB,CAAC,CAAC,CAAC;IACrE,CAAC;IACD,WAAW;QACT,OAAO,EAAE,MAAM,KAAI,CAAC,EAAE,CAAC;IACzB,CAAC;IACD,eAAe;QACb,WAAW;IACb,CAAC;CACF,CAAC","sourcesContent":["import { SpotifyError } from \"./ExpoSpotifySDK.types\";\n\nfunction notImplementedReject(method: string): SpotifyError {\n return new SpotifyError(\n \"INVALID_CONFIG\",\n `expo-spotify-sdk: ${method} is not implemented on web yet (planned for v0.7.0).`,\n );\n}\n\nexport default {\n isAvailable(): boolean {\n return false;\n },\n authenticateAsync(): Promise<never> {\n return Promise.reject(notImplementedReject(\"authenticateAsync\"));\n },\n refreshSessionAsync(): Promise<never> {\n return Promise.reject(notImplementedReject(\"refreshSessionAsync\"));\n },\n addListener(): { remove(): void } {\n return { remove() {} };\n },\n removeListeners(): void {\n /* no-op */\n },\n};\n"]}
package/build/index.d.ts CHANGED
@@ -1,8 +1,45 @@
1
- import { SpotifyConfig, SpotifySession } from "./ExpoSpotifySDK.types";
1
+ import { EventSubscription } from "expo-modules-core";
2
+ import { SpotifyConfig, SpotifyError, SpotifyRefreshConfig, SpotifySession, SpotifySessionChangeEvent } from "./ExpoSpotifySDK.types";
3
+ /**
4
+ * Returns `true` if the Spotify app is installed on the device.
5
+ * Always returns `false` on web.
6
+ */
2
7
  declare function isAvailable(): boolean;
8
+ /**
9
+ * Starts a Spotify OAuth flow. Resolves with a {@link SpotifySession};
10
+ * rejects with a {@link SpotifyError} carrying a `code`.
11
+ */
3
12
  declare function authenticateAsync(config: SpotifyConfig): Promise<SpotifySession>;
13
+ /**
14
+ * Exchanges a refresh token for a new access token via your token refresh
15
+ * server. Resolves with a fresh {@link SpotifySession}; rejects with a
16
+ * {@link SpotifyError}.
17
+ */
18
+ declare function refreshSessionAsync(config: SpotifyRefreshConfig): Promise<SpotifySession>;
19
+ /**
20
+ * Subscribes to session lifecycle events emitted by the native module.
21
+ *
22
+ * Events are fired for every `authenticateAsync` and `refreshSessionAsync`
23
+ * call, regardless of whether the call was awaited. Useful for persisting
24
+ * tokens in a central store without coupling the store to the call sites.
25
+ *
26
+ * Returns a {@link Subscription} — call `.remove()` to unsubscribe.
27
+ *
28
+ * @example
29
+ * ```ts
30
+ * const sub = addSessionChangeListener((event) => {
31
+ * if (event.type === "didInitiate" || event.type === "didRenew") {
32
+ * store.setSession(event.session);
33
+ * }
34
+ * });
35
+ * // later:
36
+ * sub.remove();
37
+ * ```
38
+ */
39
+ declare function addSessionChangeListener(listener: (event: SpotifySessionChangeEvent) => void): EventSubscription;
4
40
  declare const Authenticate: {
5
41
  authenticateAsync: typeof authenticateAsync;
6
42
  };
7
- export { isAvailable, Authenticate };
43
+ export { isAvailable, authenticateAsync, refreshSessionAsync, addSessionChangeListener, Authenticate, SpotifyError, };
44
+ export type { SpotifyConfig, SpotifyRefreshConfig, SpotifySession, SpotifySessionChangeEvent, SpotifyErrorCode, SpotifyScope, } from "./ExpoSpotifySDK.types";
8
45
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAGvE,iBAAS,WAAW,IAAI,OAAO,CAE9B;AAED,iBAAS,iBAAiB,CAAC,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,cAAc,CAAC,CAMzE;AAED,QAAA,MAAM,YAAY;;CAEjB,CAAC;AAEF,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAY,MAAM,mBAAmB,CAAC;AAEhE,OAAO,EACL,aAAa,EACb,YAAY,EAEZ,oBAAoB,EACpB,cAAc,EACd,yBAAyB,EAC1B,MAAM,wBAAwB,CAAC;AAWhC;;;GAGG;AACH,iBAAS,WAAW,IAAI,OAAO,CAE9B;AAED;;;GAGG;AACH,iBAAS,iBAAiB,CAAC,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,cAAc,CAAC,CAqBzE;AAgCD;;;;GAIG;AACH,iBAAS,mBAAmB,CAC1B,MAAM,EAAE,oBAAoB,GAC3B,OAAO,CAAC,cAAc,CAAC,CAczB;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,iBAAS,wBAAwB,CAC/B,QAAQ,EAAE,CAAC,KAAK,EAAE,yBAAyB,KAAK,IAAI,GACnD,iBAAiB,CAKnB;AAgCD,QAAA,MAAM,YAAY;;CAEjB,CAAC;AAEF,OAAO,EACL,WAAW,EACX,iBAAiB,EACjB,mBAAmB,EACnB,wBAAwB,EACxB,YAAY,EACZ,YAAY,GACb,CAAC;AACF,YAAY,EACV,aAAa,EACb,oBAAoB,EACpB,cAAc,EACd,yBAAyB,EACzB,gBAAgB,EAChB,YAAY,GACb,MAAM,wBAAwB,CAAC"}
package/build/index.js CHANGED
@@ -1,15 +1,128 @@
1
+ import { Platform } from "expo-modules-core";
2
+ import { SpotifyError, } from "./ExpoSpotifySDK.types";
1
3
  import ExpoSpotifySDKModule from "./ExpoSpotifySDKModule";
4
+ const ANDROID_TOKEN_FLOW_WARNING = "[expo-spotify-sdk] You are using authenticateAsync on Android without a " +
5
+ "tokenSwapURL. The Spotify Android SDK does NOT return a refresh token or " +
6
+ "the actual granted scopes through this path; see the README's " +
7
+ "'Android implicit (TOKEN) flow is not recommended' section.";
8
+ let warnedAboutAndroidTokenFlow = false;
9
+ /**
10
+ * Returns `true` if the Spotify app is installed on the device.
11
+ * Always returns `false` on web.
12
+ */
2
13
  function isAvailable() {
3
14
  return ExpoSpotifySDKModule.isAvailable();
4
15
  }
16
+ /**
17
+ * Starts a Spotify OAuth flow. Resolves with a {@link SpotifySession};
18
+ * rejects with a {@link SpotifyError} carrying a `code`.
19
+ */
5
20
  function authenticateAsync(config) {
6
- if (!config.scopes || config.scopes?.length === 0) {
7
- throw new Error("scopes are required");
21
+ if (!config.scopes || config.scopes.length === 0) {
22
+ return Promise.reject(new SpotifyError("INVALID_CONFIG", "scopes must contain at least one entry"));
8
23
  }
9
- return ExpoSpotifySDKModule.authenticateAsync(config);
24
+ if (Platform.OS === "android" &&
25
+ !config.tokenSwapURL &&
26
+ !warnedAboutAndroidTokenFlow) {
27
+ warnedAboutAndroidTokenFlow = true;
28
+ console.warn(ANDROID_TOKEN_FLOW_WARNING);
29
+ }
30
+ return ExpoSpotifySDKModule.authenticateAsync(config)
31
+ .then(normaliseSession)
32
+ .catch(rethrowAsSpotifyError);
33
+ }
34
+ function normaliseSession(raw) {
35
+ if (!raw || typeof raw !== "object") {
36
+ throw new SpotifyError("UNKNOWN", "Native module returned a non-object session");
37
+ }
38
+ const r = raw;
39
+ const accessToken = r.accessToken;
40
+ if (typeof accessToken !== "string" || accessToken.length === 0) {
41
+ throw new SpotifyError("UNKNOWN", "Session is missing accessToken");
42
+ }
43
+ const expirationDate = r.expirationDate;
44
+ if (typeof expirationDate !== "number") {
45
+ throw new SpotifyError("UNKNOWN", "Session is missing expirationDate");
46
+ }
47
+ const refreshTokenRaw = r.refreshToken;
48
+ const refreshToken = typeof refreshTokenRaw === "string" && refreshTokenRaw.length > 0
49
+ ? refreshTokenRaw
50
+ : null;
51
+ const scopesRaw = r.scopes;
52
+ const scopes = Array.isArray(scopesRaw)
53
+ ? scopesRaw.filter((s) => typeof s === "string")
54
+ : [];
55
+ return { accessToken, refreshToken, expirationDate, scopes };
56
+ }
57
+ /**
58
+ * Exchanges a refresh token for a new access token via your token refresh
59
+ * server. Resolves with a fresh {@link SpotifySession}; rejects with a
60
+ * {@link SpotifyError}.
61
+ */
62
+ function refreshSessionAsync(config) {
63
+ if (!config.refreshToken) {
64
+ return Promise.reject(new SpotifyError("INVALID_CONFIG", "refreshToken is required"));
65
+ }
66
+ if (!config.tokenRefreshURL) {
67
+ return Promise.reject(new SpotifyError("INVALID_CONFIG", "tokenRefreshURL is required"));
68
+ }
69
+ return ExpoSpotifySDKModule.refreshSessionAsync(config)
70
+ .then(normaliseSession)
71
+ .catch(rethrowAsSpotifyError);
72
+ }
73
+ /**
74
+ * Subscribes to session lifecycle events emitted by the native module.
75
+ *
76
+ * Events are fired for every `authenticateAsync` and `refreshSessionAsync`
77
+ * call, regardless of whether the call was awaited. Useful for persisting
78
+ * tokens in a central store without coupling the store to the call sites.
79
+ *
80
+ * Returns a {@link Subscription} — call `.remove()` to unsubscribe.
81
+ *
82
+ * @example
83
+ * ```ts
84
+ * const sub = addSessionChangeListener((event) => {
85
+ * if (event.type === "didInitiate" || event.type === "didRenew") {
86
+ * store.setSession(event.session);
87
+ * }
88
+ * });
89
+ * // later:
90
+ * sub.remove();
91
+ * ```
92
+ */
93
+ function addSessionChangeListener(listener) {
94
+ return ExpoSpotifySDKModule.addListener("onSessionChange", listener);
95
+ }
96
+ const ERROR_PREFIX_RE = /^([A-Z_][A-Z0-9_]*):\s*(.*)$/s;
97
+ const VALID_CODES = new Set([
98
+ "USER_CANCELLED",
99
+ "AUTH_IN_PROGRESS",
100
+ "INVALID_CONFIG",
101
+ "NETWORK_ERROR",
102
+ "TOKEN_SWAP_FAILED",
103
+ "TOKEN_SWAP_PARSE_ERROR",
104
+ "SPOTIFY_NOT_INSTALLED",
105
+ "AUTH_ERROR",
106
+ "UNKNOWN",
107
+ ]);
108
+ function rethrowAsSpotifyError(err) {
109
+ if (err instanceof SpotifyError)
110
+ throw err;
111
+ if (err instanceof Error) {
112
+ const m = err.message.match(ERROR_PREFIX_RE);
113
+ if (m && VALID_CODES.has(m[1])) {
114
+ throw new SpotifyError(m[1], m[2]);
115
+ }
116
+ const maybeCode = err.code;
117
+ if (maybeCode && VALID_CODES.has(maybeCode)) {
118
+ throw new SpotifyError(maybeCode, err.message);
119
+ }
120
+ throw new SpotifyError("UNKNOWN", err.message);
121
+ }
122
+ throw new SpotifyError("UNKNOWN", String(err));
10
123
  }
11
124
  const Authenticate = {
12
125
  authenticateAsync,
13
126
  };
14
- export { isAvailable, Authenticate };
127
+ export { isAvailable, authenticateAsync, refreshSessionAsync, addSessionChangeListener, Authenticate, SpotifyError, };
15
128
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,oBAAoB,MAAM,wBAAwB,CAAC;AAE1D,SAAS,WAAW;IAClB,OAAO,oBAAoB,CAAC,WAAW,EAAE,CAAC;AAC5C,CAAC;AAED,SAAS,iBAAiB,CAAC,MAAqB;IAC9C,IAAI,CAAC,MAAM,CAAC,MAAM,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,KAAK,CAAC,EAAE,CAAC;QAClD,MAAM,IAAI,KAAK,CAAC,qBAAqB,CAAC,CAAC;IACzC,CAAC;IAED,OAAO,oBAAoB,CAAC,iBAAiB,CAAC,MAAM,CAAC,CAAC;AACxD,CAAC;AAED,MAAM,YAAY,GAAG;IACnB,iBAAiB;CAClB,CAAC;AAEF,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,CAAC","sourcesContent":["import { SpotifyConfig, SpotifySession } from \"./ExpoSpotifySDK.types\";\nimport ExpoSpotifySDKModule from \"./ExpoSpotifySDKModule\";\n\nfunction isAvailable(): boolean {\n return ExpoSpotifySDKModule.isAvailable();\n}\n\nfunction authenticateAsync(config: SpotifyConfig): Promise<SpotifySession> {\n if (!config.scopes || config.scopes?.length === 0) {\n throw new Error(\"scopes are required\");\n }\n\n return ExpoSpotifySDKModule.authenticateAsync(config);\n}\n\nconst Authenticate = {\n authenticateAsync,\n};\n\nexport { isAvailable, Authenticate };\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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","sourcesContent":["import { EventSubscription, Platform } from \"expo-modules-core\";\n\nimport {\n SpotifyConfig,\n SpotifyError,\n SpotifyErrorCode,\n SpotifyRefreshConfig,\n SpotifySession,\n SpotifySessionChangeEvent,\n} from \"./ExpoSpotifySDK.types\";\nimport ExpoSpotifySDKModule from \"./ExpoSpotifySDKModule\";\n\nconst ANDROID_TOKEN_FLOW_WARNING =\n \"[expo-spotify-sdk] You are using authenticateAsync on Android without a \" +\n \"tokenSwapURL. The Spotify Android SDK does NOT return a refresh token or \" +\n \"the actual granted scopes through this path; see the README's \" +\n \"'Android implicit (TOKEN) flow is not recommended' section.\";\n\nlet warnedAboutAndroidTokenFlow = false;\n\n/**\n * Returns `true` if the Spotify app is installed on the device.\n * Always returns `false` on web.\n */\nfunction isAvailable(): boolean {\n return ExpoSpotifySDKModule.isAvailable();\n}\n\n/**\n * Starts a Spotify OAuth flow. Resolves with a {@link SpotifySession};\n * rejects with a {@link SpotifyError} carrying a `code`.\n */\nfunction authenticateAsync(config: SpotifyConfig): Promise<SpotifySession> {\n if (!config.scopes || config.scopes.length === 0) {\n return Promise.reject(\n new SpotifyError(\n \"INVALID_CONFIG\",\n \"scopes must contain at least one entry\",\n ),\n );\n }\n if (\n Platform.OS === \"android\" &&\n !config.tokenSwapURL &&\n !warnedAboutAndroidTokenFlow\n ) {\n warnedAboutAndroidTokenFlow = true;\n\n console.warn(ANDROID_TOKEN_FLOW_WARNING);\n }\n return ExpoSpotifySDKModule.authenticateAsync(config)\n .then(normaliseSession)\n .catch(rethrowAsSpotifyError);\n}\n\nfunction normaliseSession(raw: unknown): SpotifySession {\n if (!raw || typeof raw !== \"object\") {\n throw new SpotifyError(\n \"UNKNOWN\",\n \"Native module returned a non-object session\",\n );\n }\n const r = raw as Record<string, unknown>;\n const accessToken = r.accessToken;\n if (typeof accessToken !== \"string\" || accessToken.length === 0) {\n throw new SpotifyError(\"UNKNOWN\", \"Session is missing accessToken\");\n }\n const expirationDate = r.expirationDate;\n if (typeof expirationDate !== \"number\") {\n throw new SpotifyError(\"UNKNOWN\", \"Session is missing expirationDate\");\n }\n const refreshTokenRaw = r.refreshToken;\n const refreshToken =\n typeof refreshTokenRaw === \"string\" && refreshTokenRaw.length > 0\n ? refreshTokenRaw\n : null;\n const scopesRaw = r.scopes;\n const scopes = Array.isArray(scopesRaw)\n ? (scopesRaw.filter(\n (s) => typeof s === \"string\",\n ) as SpotifySession[\"scopes\"])\n : [];\n return { accessToken, refreshToken, expirationDate, scopes };\n}\n\n/**\n * Exchanges a refresh token for a new access token via your token refresh\n * server. Resolves with a fresh {@link SpotifySession}; rejects with a\n * {@link SpotifyError}.\n */\nfunction refreshSessionAsync(\n config: SpotifyRefreshConfig,\n): Promise<SpotifySession> {\n if (!config.refreshToken) {\n return Promise.reject(\n new SpotifyError(\"INVALID_CONFIG\", \"refreshToken is required\"),\n );\n }\n if (!config.tokenRefreshURL) {\n return Promise.reject(\n new SpotifyError(\"INVALID_CONFIG\", \"tokenRefreshURL is required\"),\n );\n }\n return ExpoSpotifySDKModule.refreshSessionAsync(config)\n .then(normaliseSession)\n .catch(rethrowAsSpotifyError);\n}\n\n/**\n * Subscribes to session lifecycle events emitted by the native module.\n *\n * Events are fired for every `authenticateAsync` and `refreshSessionAsync`\n * call, regardless of whether the call was awaited. Useful for persisting\n * tokens in a central store without coupling the store to the call sites.\n *\n * Returns a {@link Subscription} — call `.remove()` to unsubscribe.\n *\n * @example\n * ```ts\n * const sub = addSessionChangeListener((event) => {\n * if (event.type === \"didInitiate\" || event.type === \"didRenew\") {\n * store.setSession(event.session);\n * }\n * });\n * // later:\n * sub.remove();\n * ```\n */\nfunction addSessionChangeListener(\n listener: (event: SpotifySessionChangeEvent) => void,\n): EventSubscription {\n return ExpoSpotifySDKModule.addListener(\n \"onSessionChange\",\n listener,\n ) as EventSubscription;\n}\n\nconst ERROR_PREFIX_RE = /^([A-Z_][A-Z0-9_]*):\\s*(.*)$/s;\n\nconst VALID_CODES: ReadonlySet<SpotifyErrorCode> = new Set<SpotifyErrorCode>([\n \"USER_CANCELLED\",\n \"AUTH_IN_PROGRESS\",\n \"INVALID_CONFIG\",\n \"NETWORK_ERROR\",\n \"TOKEN_SWAP_FAILED\",\n \"TOKEN_SWAP_PARSE_ERROR\",\n \"SPOTIFY_NOT_INSTALLED\",\n \"AUTH_ERROR\",\n \"UNKNOWN\",\n]);\n\nfunction rethrowAsSpotifyError(err: unknown): never {\n if (err instanceof SpotifyError) throw err;\n if (err instanceof Error) {\n const m = err.message.match(ERROR_PREFIX_RE);\n if (m && VALID_CODES.has(m[1] as SpotifyErrorCode)) {\n throw new SpotifyError(m[1] as SpotifyErrorCode, m[2]);\n }\n const maybeCode = (err as Error & { code?: string }).code;\n if (maybeCode && VALID_CODES.has(maybeCode as SpotifyErrorCode)) {\n throw new SpotifyError(maybeCode as SpotifyErrorCode, err.message);\n }\n throw new SpotifyError(\"UNKNOWN\", err.message);\n }\n throw new SpotifyError(\"UNKNOWN\", String(err));\n}\n\nconst Authenticate = {\n authenticateAsync,\n};\n\nexport {\n isAvailable,\n authenticateAsync,\n refreshSessionAsync,\n addSessionChangeListener,\n Authenticate,\n SpotifyError,\n};\nexport type {\n SpotifyConfig,\n SpotifyRefreshConfig,\n SpotifySession,\n SpotifySessionChangeEvent,\n SpotifyErrorCode,\n SpotifyScope,\n} from \"./ExpoSpotifySDK.types\";\n"]}
@@ -2,7 +2,7 @@
2
2
  "platforms": ["ios", "android", "web"],
3
3
  "ios": {
4
4
  "modules": ["ExpoSpotifySDKModule"],
5
- "appDelegateSubscribers": ["ExpoSpotifyAuthDelegate"]
5
+ "appDelegateSubscribers": ["ExpoSpotifyAppDelegate"]
6
6
  },
7
7
  "android": {
8
8
  "modules": ["expo.modules.spotifysdk.ExpoSpotifySDKModule"]
@@ -0,0 +1,14 @@
1
+ import ExpoModulesCore
2
+ import SpotifyiOS
3
+
4
+ /// Delivers Spotify auth redirect URLs received via the legacy
5
+ /// `application(_:open:url:)` AppDelegate path.
6
+ public class ExpoSpotifyAppDelegate: ExpoAppDelegateSubscriber {
7
+ public func application(
8
+ _ app: UIApplication,
9
+ open url: URL,
10
+ options: [UIApplication.OpenURLOptionsKey: Any] = [:]
11
+ ) -> Bool {
12
+ return SpotifyAuthCoordinator.shared?.handleOpenURL(url, options: options) ?? false
13
+ }
14
+ }
@@ -1,20 +1,38 @@
1
+ import Foundation
1
2
  import SpotifyiOS
2
3
 
3
- struct ExpoSpotifyConfiguration: Codable {
4
- let clientID: String
5
- let host: String
6
- let scheme: String
7
-
8
- var redirectURL: URL? {
9
- return URL(string: "\(scheme)://\(host)")
4
+ /// Strongly-typed view of the `ExpoSpotifySDK` Info.plist dictionary written
5
+ /// by the Expo config plugin. Values are validated up-front; missing or empty
6
+ /// entries return nil so callers can fail with a structured error.
7
+ struct ExpoSpotifyConfiguration {
8
+ let clientID: String
9
+ let host: String
10
+ let scheme: String
11
+
12
+ var redirectURL: URL? {
13
+ return URL(string: "\(scheme)://\(host)")
14
+ }
15
+
16
+ var sptConfiguration: SPTConfiguration? {
17
+ guard let redirectURL = redirectURL else { return nil }
18
+ return SPTConfiguration(clientID: clientID, redirectURL: redirectURL)
19
+ }
20
+
21
+ static func fromInfoPlist(bundle: Bundle = .main) -> ExpoSpotifyConfiguration? {
22
+ guard let dict = bundle.object(forInfoDictionaryKey: "ExpoSpotifySDK") as? [String: Any] else {
23
+ return nil
10
24
  }
11
-
12
- init(clientID: String = "defaultClientID",
13
- host: String = "defaultHost",
14
- scheme: String = "defaultScheme"
15
- ) {
16
- self.clientID = clientID
17
- self.host = host
18
- self.scheme = scheme
25
+ guard
26
+ let clientID = (dict["clientID"] as? String)?.nilIfEmpty,
27
+ let host = (dict["host"] as? String)?.nilIfEmpty,
28
+ let scheme = (dict["scheme"] as? String)?.nilIfEmpty
29
+ else {
30
+ return nil
19
31
  }
32
+ return ExpoSpotifyConfiguration(clientID: clientID, host: host, scheme: scheme)
33
+ }
34
+ }
35
+
36
+ private extension String {
37
+ var nilIfEmpty: String? { isEmpty ? nil : self }
20
38
  }
@@ -10,15 +10,13 @@ Pod::Spec.new do |s|
10
10
  s.license = package['license']
11
11
  s.author = package['author']
12
12
  s.homepage = package['homepage']
13
- s.platform = :ios, '13.0'
14
- s.swift_version = '5.4'
13
+ s.platform = :ios, '15.1'
14
+ s.swift_version = '5.9'
15
15
  s.source = { git: 'https://github.com/wwdrew/expo-spotify-sdk' }
16
16
  s.static_framework = true
17
17
 
18
18
  s.dependency 'ExpoModulesCore'
19
- s.dependency 'PromiseKit', "~> 6.8"
20
19
 
21
- # Swift/Objective-C compatibility
22
20
  s.pod_target_xcconfig = {
23
21
  'DEFINES_MODULE' => 'YES',
24
22
  'SWIFT_COMPILATION_MODE' => 'wholemodule'