@wwdrew/expo-spotify-sdk 0.8.0 → 1.0.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 (80) hide show
  1. package/README.md +378 -138
  2. package/android/build.gradle +10 -0
  3. package/android/consumer-rules.pro +5 -0
  4. package/android/libs/SETUP.md +29 -0
  5. package/android/src/main/java/expo/modules/spotifysdk/ExpoSpotifySDKModule.kt +146 -9
  6. package/android/src/main/java/expo/modules/spotifysdk/SpotifyAppRemoteCoordinator.kt +556 -0
  7. package/android/src/main/java/expo/modules/spotifysdk/SpotifyErrors.kt +112 -0
  8. package/build/ExpoSpotifySDKModule.web.d.ts +23 -0
  9. package/build/ExpoSpotifySDKModule.web.d.ts.map +1 -1
  10. package/build/ExpoSpotifySDKModule.web.js +74 -5
  11. package/build/ExpoSpotifySDKModule.web.js.map +1 -1
  12. package/build/app-remote/error.d.ts +8 -0
  13. package/build/app-remote/error.d.ts.map +1 -0
  14. package/build/app-remote/error.js +10 -0
  15. package/build/app-remote/error.js.map +1 -0
  16. package/build/app-remote/index.d.ts +79 -0
  17. package/build/app-remote/index.d.ts.map +1 -0
  18. package/build/app-remote/index.js +110 -0
  19. package/build/app-remote/index.js.map +1 -0
  20. package/build/auth/error.d.ts +8 -0
  21. package/build/auth/error.d.ts.map +1 -0
  22. package/build/auth/error.js +10 -0
  23. package/build/auth/error.js.map +1 -0
  24. package/build/auth/index.d.ts +124 -0
  25. package/build/auth/index.d.ts.map +1 -0
  26. package/build/auth/index.js +162 -0
  27. package/build/auth/index.js.map +1 -0
  28. package/build/content/error.d.ts +8 -0
  29. package/build/content/error.d.ts.map +1 -0
  30. package/build/content/error.js +10 -0
  31. package/build/content/error.js.map +1 -0
  32. package/build/content/index.d.ts +38 -0
  33. package/build/content/index.d.ts.map +1 -0
  34. package/build/content/index.js +56 -0
  35. package/build/content/index.js.map +1 -0
  36. package/build/error.d.ts +24 -0
  37. package/build/error.d.ts.map +1 -0
  38. package/build/error.js +25 -0
  39. package/build/error.js.map +1 -0
  40. package/build/hooks/index.d.ts +90 -0
  41. package/build/hooks/index.d.ts.map +1 -0
  42. package/build/hooks/index.js +326 -0
  43. package/build/hooks/index.js.map +1 -0
  44. package/build/images/error.d.ts +8 -0
  45. package/build/images/error.d.ts.map +1 -0
  46. package/build/images/error.js +10 -0
  47. package/build/images/error.js.map +1 -0
  48. package/build/images/index.d.ts +36 -0
  49. package/build/images/index.d.ts.map +1 -0
  50. package/build/images/index.js +59 -0
  51. package/build/images/index.js.map +1 -0
  52. package/build/index.d.ts +44 -56
  53. package/build/index.d.ts.map +1 -1
  54. package/build/index.js +49 -141
  55. package/build/index.js.map +1 -1
  56. package/build/player/error.d.ts +8 -0
  57. package/build/player/error.d.ts.map +1 -0
  58. package/build/player/error.js +10 -0
  59. package/build/player/error.js.map +1 -0
  60. package/build/player/index.d.ts +140 -0
  61. package/build/player/index.d.ts.map +1 -0
  62. package/build/player/index.js +136 -0
  63. package/build/player/index.js.map +1 -0
  64. package/build/uri/index.d.ts +55 -0
  65. package/build/uri/index.d.ts.map +1 -0
  66. package/build/uri/index.js +57 -0
  67. package/build/uri/index.js.map +1 -0
  68. package/build/user/error.d.ts +8 -0
  69. package/build/user/error.d.ts.map +1 -0
  70. package/build/user/error.js +10 -0
  71. package/build/user/error.js.map +1 -0
  72. package/build/user/index.d.ts +44 -0
  73. package/build/user/index.d.ts.map +1 -0
  74. package/build/user/index.js +97 -0
  75. package/build/user/index.js.map +1 -0
  76. package/ios/ExpoSpotifySDKModule.swift +227 -5
  77. package/ios/SpotifyAppRemoteCoordinator.swift +939 -0
  78. package/ios/SpotifyAuthCoordinator.swift +53 -13
  79. package/ios/SpotifyTokenRefreshClient.swift +28 -0
  80. package/package.json +4 -3
@@ -0,0 +1,57 @@
1
+ const URI_RE = /^spotify:(track|album|playlist|artist|show|episode):([A-Za-z0-9]+)$/;
2
+ /**
3
+ * Helpers for constructing, validating and decomposing {@link SpotifyURI}
4
+ * values.
5
+ *
6
+ * @example
7
+ * ```ts
8
+ * const uri = SpotifyURI.from("spotify:track:4uLU6hMCjMI75M1A2tKUQC");
9
+ * const { type, id } = SpotifyURI.parse(uri);
10
+ * const rebuilt = SpotifyURI.build("track", id);
11
+ * ```
12
+ */
13
+ export const SpotifyURI = {
14
+ /**
15
+ * Cast `uri` to {@link SpotifyURI} after validation.
16
+ * Throws a plain `Error` if the URI does not match the `spotify:<type>:<id>`
17
+ * format — use at app-code call sites where you want early feedback.
18
+ */
19
+ from(uri) {
20
+ if (!SpotifyURI.isValid(uri)) {
21
+ throw new Error(`Invalid Spotify URI: "${uri}". Expected spotify:<type>:<id> where ` +
22
+ `<type> is one of: track, album, playlist, artist, show, episode.`);
23
+ }
24
+ return uri;
25
+ },
26
+ /**
27
+ * Cast `uri` to {@link SpotifyURI} without validation.
28
+ * Use only for URIs that originate from the Spotify SDK itself (i.e. already
29
+ * known-valid values coming back over the bridge).
30
+ */
31
+ unsafe(uri) {
32
+ return uri;
33
+ },
34
+ /**
35
+ * Decompose a {@link SpotifyURI} into its `{ type, id }` parts.
36
+ */
37
+ parse(uri) {
38
+ const m = uri.match(URI_RE);
39
+ if (!m) {
40
+ throw new Error(`Cannot parse Spotify URI: "${uri}"`);
41
+ }
42
+ return { type: m[1], id: m[2] };
43
+ },
44
+ /**
45
+ * Build a {@link SpotifyURI} from a resource type and ID.
46
+ */
47
+ build(type, id) {
48
+ return `spotify:${type}:${id}`;
49
+ },
50
+ /**
51
+ * Type-guard: returns `true` if `uri` is a valid Spotify URI string.
52
+ */
53
+ isValid(uri) {
54
+ return URI_RE.test(uri);
55
+ },
56
+ };
57
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/uri/index.ts"],"names":[],"mappings":"AAoBA,MAAM,MAAM,GACV,qEAAqE,CAAC;AAExE;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG;IACxB;;;;OAIG;IACH,IAAI,CAAC,GAAW;QACd,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;YAC7B,MAAM,IAAI,KAAK,CACb,yBAAyB,GAAG,wCAAwC;gBAClE,kEAAkE,CACrE,CAAC;QACJ,CAAC;QACD,OAAO,GAAiB,CAAC;IAC3B,CAAC;IAED;;;;OAIG;IACH,MAAM,CAAC,GAAW;QAChB,OAAO,GAAiB,CAAC;IAC3B,CAAC;IAED;;OAEG;IACH,KAAK,CAAC,GAAe;QACnB,MAAM,CAAC,GAAG,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;QAC5B,IAAI,CAAC,CAAC,EAAE,CAAC;YACP,MAAM,IAAI,KAAK,CAAC,8BAA8B,GAAG,GAAG,CAAC,CAAC;QACxD,CAAC;QACD,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAwB,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IACzD,CAAC;IAED;;OAEG;IACH,KAAK,CAAC,IAAyB,EAAE,EAAU;QACzC,OAAO,WAAW,IAAI,IAAI,EAAE,EAAgB,CAAC;IAC/C,CAAC;IAED;;OAEG;IACH,OAAO,CAAC,GAAW;QACjB,OAAO,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAC1B,CAAC;CACO,CAAC","sourcesContent":["/**\n * The six Spotify resource types accepted by App Remote APIs.\n */\nexport type SpotifyResourceType =\n | \"track\"\n | \"album\"\n | \"playlist\"\n | \"artist\"\n | \"show\"\n | \"episode\";\n\n/**\n * Branded string type for Spotify URIs (`spotify:<type>:<id>`).\n *\n * Construct via `SpotifyURI.from(str)` (validates) or\n * `SpotifyURI.unsafe(str)` (skips validation — use only when the source is\n * trusted, e.g. a URI that came back from the Spotify SDK itself).\n */\nexport type SpotifyURI = string & { readonly __brand: \"SpotifyURI\" };\n\nconst URI_RE =\n /^spotify:(track|album|playlist|artist|show|episode):([A-Za-z0-9]+)$/;\n\n/**\n * Helpers for constructing, validating and decomposing {@link SpotifyURI}\n * values.\n *\n * @example\n * ```ts\n * const uri = SpotifyURI.from(\"spotify:track:4uLU6hMCjMI75M1A2tKUQC\");\n * const { type, id } = SpotifyURI.parse(uri);\n * const rebuilt = SpotifyURI.build(\"track\", id);\n * ```\n */\nexport const SpotifyURI = {\n /**\n * Cast `uri` to {@link SpotifyURI} after validation.\n * Throws a plain `Error` if the URI does not match the `spotify:<type>:<id>`\n * format — use at app-code call sites where you want early feedback.\n */\n from(uri: string): SpotifyURI {\n if (!SpotifyURI.isValid(uri)) {\n throw new Error(\n `Invalid Spotify URI: \"${uri}\". Expected spotify:<type>:<id> where ` +\n `<type> is one of: track, album, playlist, artist, show, episode.`,\n );\n }\n return uri as SpotifyURI;\n },\n\n /**\n * Cast `uri` to {@link SpotifyURI} without validation.\n * Use only for URIs that originate from the Spotify SDK itself (i.e. already\n * known-valid values coming back over the bridge).\n */\n unsafe(uri: string): SpotifyURI {\n return uri as SpotifyURI;\n },\n\n /**\n * Decompose a {@link SpotifyURI} into its `{ type, id }` parts.\n */\n parse(uri: SpotifyURI): { type: SpotifyResourceType; id: string } {\n const m = uri.match(URI_RE);\n if (!m) {\n throw new Error(`Cannot parse Spotify URI: \"${uri}\"`);\n }\n return { type: m[1] as SpotifyResourceType, id: m[2] };\n },\n\n /**\n * Build a {@link SpotifyURI} from a resource type and ID.\n */\n build(type: SpotifyResourceType, id: string): SpotifyURI {\n return `spotify:${type}:${id}` as SpotifyURI;\n },\n\n /**\n * Type-guard: returns `true` if `uri` is a valid Spotify URI string.\n */\n isValid(uri: string): uri is SpotifyURI {\n return URI_RE.test(uri);\n },\n} as const;\n"]}
@@ -0,0 +1,8 @@
1
+ import { SpotifyError } from "../error";
2
+ export type UserErrorCode = "NOT_CONNECTED" | "CONNECTION_LOST" | "INVALID_URI" | "OPERATION_NOT_ALLOWED" | "UNKNOWN";
3
+ export declare class UserError extends SpotifyError {
4
+ readonly namespace: "User";
5
+ readonly code: UserErrorCode;
6
+ constructor(code: UserErrorCode, message: string);
7
+ }
8
+ //# sourceMappingURL=error.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"error.d.ts","sourceRoot":"","sources":["../../src/user/error.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAExC,MAAM,MAAM,aAAa,GACrB,eAAe,GACf,iBAAiB,GACjB,aAAa,GACb,uBAAuB,GACvB,SAAS,CAAC;AAEd,qBAAa,SAAU,SAAQ,YAAY;IACzC,QAAQ,CAAC,SAAS,EAAG,MAAM,CAAU;IACrC,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC;gBAEjB,IAAI,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM;CAIjD"}
@@ -0,0 +1,10 @@
1
+ import { SpotifyError } from "../error";
2
+ export class UserError extends SpotifyError {
3
+ namespace = "User";
4
+ code;
5
+ constructor(code, message) {
6
+ super(message);
7
+ this.code = code;
8
+ }
9
+ }
10
+ //# sourceMappingURL=error.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"error.js","sourceRoot":"","sources":["../../src/user/error.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AASxC,MAAM,OAAO,SAAU,SAAQ,YAAY;IAChC,SAAS,GAAG,MAAe,CAAC;IAC5B,IAAI,CAAgB;IAE7B,YAAY,IAAmB,EAAE,OAAe;QAC9C,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IACnB,CAAC;CACF","sourcesContent":["import { SpotifyError } from \"../error\";\n\nexport type UserErrorCode =\n | \"NOT_CONNECTED\"\n | \"CONNECTION_LOST\"\n | \"INVALID_URI\"\n | \"OPERATION_NOT_ALLOWED\"\n | \"UNKNOWN\";\n\nexport class UserError extends SpotifyError {\n readonly namespace = \"User\" as const;\n readonly code: UserErrorCode;\n\n constructor(code: UserErrorCode, message: string) {\n super(message);\n this.code = code;\n }\n}\n"]}
@@ -0,0 +1,44 @@
1
+ export type { UserErrorCode } from "./error";
2
+ export { UserError } from "./error";
3
+ import { EventSubscription } from "expo-modules-core";
4
+ import type { SpotifyURI as SpotifyURIType } from "../uri";
5
+ /** Capabilities of the current user in Spotify's App Remote context. */
6
+ export interface Capabilities {
7
+ /** `true` for Premium users with on-demand playback capability. */
8
+ canPlayOnDemand: boolean;
9
+ }
10
+ /** Library save state for a specific album/track URI. */
11
+ export interface LibraryState {
12
+ uri: string;
13
+ isAdded: boolean;
14
+ canAdd: boolean;
15
+ }
16
+ /**
17
+ * Spotify User namespace. Capabilities and library-state operations.
18
+ * Requires `AppRemote.connect()` to be resolved before any call.
19
+ */
20
+ export declare const User: {
21
+ /** Returns the current user's capabilities. */
22
+ readonly getCapabilities: () => Promise<Capabilities>;
23
+ /** Returns the current library state for a track or album URI. */
24
+ readonly getLibraryState: (uri: SpotifyURIType) => Promise<LibraryState>;
25
+ /** Adds a track or album URI to the user's library. */
26
+ readonly addToLibrary: (uri: SpotifyURIType) => Promise<void>;
27
+ /** Removes a track or album URI from the user's library. */
28
+ readonly removeFromLibrary: (uri: SpotifyURIType) => Promise<void>;
29
+ /**
30
+ * Subscribes to user-scoped events.
31
+ * Supported event: `"capabilitiesChange"`.
32
+ */
33
+ readonly addListener: (event: "capabilitiesChange", listener: (event: Capabilities) => void) => EventSubscription;
34
+ /**
35
+ * Subscribes to library state changes for a specific URI.
36
+ *
37
+ * There is no native push stream for per-URI library state updates on all
38
+ * platforms, so this listener is updated whenever library mutations are
39
+ * performed via this SDK and can be manually seeded by calling
40
+ * `User.getLibraryState(uri)` before subscribing.
41
+ */
42
+ readonly addLibraryStateListener: (uri: SpotifyURIType, listener: (state: LibraryState) => void) => EventSubscription;
43
+ };
44
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/user/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAC7C,OAAO,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAMpC,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAGtD,OAAO,KAAK,EAAE,UAAU,IAAI,cAAc,EAAE,MAAM,QAAQ,CAAC;AAG3D,wEAAwE;AACxE,MAAM,WAAW,YAAY;IAC3B,mEAAmE;IACnE,eAAe,EAAE,OAAO,CAAC;CAC1B;AAED,yDAAyD;AACzD,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,OAAO,CAAC;CACjB;AA2CD;;;GAGG;AACH,eAAO,MAAM,IAAI;IACf,+CAA+C;oCAC5B,OAAO,CAAC,YAAY,CAAC;IAIxC,kEAAkE;oCAC7C,cAAc,KAAG,OAAO,CAAC,YAAY,CAAC;IAI3D,uDAAuD;iCAC/B,cAAc,KAAG,OAAO,CAAC,IAAI,CAAC;IAOtD,4DAA4D;sCAC/B,cAAc,KAAG,OAAO,CAAC,IAAI,CAAC;IAO3D;;;OAGG;kCAEM,oBAAoB,YACjB,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,KACtC,iBAAiB;IAOpB;;;;;;;OAOG;4CAEI,cAAc,YACT,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,KACtC,iBAAiB;CAiBZ,CAAC"}
@@ -0,0 +1,97 @@
1
+ export { UserError } from "./error";
2
+ import ExpoSpotifySDKModule from "../ExpoSpotifySDKModule";
3
+ import { UserError } from "./error";
4
+ // ---------------------------------------------------------------------------
5
+ // Internal helpers
6
+ // ---------------------------------------------------------------------------
7
+ const VALID_USER_CODES = new Set([
8
+ "NOT_CONNECTED",
9
+ "CONNECTION_LOST",
10
+ "INVALID_URI",
11
+ "OPERATION_NOT_ALLOWED",
12
+ "UNKNOWN",
13
+ ]);
14
+ const CAUSE_SEPARATOR = "→ Caused by: ";
15
+ function unwrapReason(message) {
16
+ const idx = message.lastIndexOf(CAUSE_SEPARATOR);
17
+ return idx === -1 ? message : message.slice(idx + CAUSE_SEPARATOR.length);
18
+ }
19
+ function rethrowAsUserError(err) {
20
+ if (err instanceof UserError)
21
+ throw err;
22
+ if (err instanceof Error) {
23
+ const reason = unwrapReason(err.message);
24
+ const maybeCode = err.code;
25
+ if (maybeCode && VALID_USER_CODES.has(maybeCode)) {
26
+ throw new UserError(maybeCode, reason);
27
+ }
28
+ throw new UserError("UNKNOWN", reason);
29
+ }
30
+ throw new UserError("UNKNOWN", String(err));
31
+ }
32
+ const libraryListeners = new Map();
33
+ function notifyLibraryState(state) {
34
+ const set = libraryListeners.get(state.uri);
35
+ if (!set)
36
+ return;
37
+ set.forEach((listener) => listener(state));
38
+ }
39
+ /**
40
+ * Spotify User namespace. Capabilities and library-state operations.
41
+ * Requires `AppRemote.connect()` to be resolved before any call.
42
+ */
43
+ export const User = {
44
+ /** Returns the current user's capabilities. */
45
+ getCapabilities() {
46
+ return ExpoSpotifySDKModule.userGetCapabilities().catch(rethrowAsUserError);
47
+ },
48
+ /** Returns the current library state for a track or album URI. */
49
+ getLibraryState(uri) {
50
+ return ExpoSpotifySDKModule.userGetLibraryState(uri).catch(rethrowAsUserError);
51
+ },
52
+ /** Adds a track or album URI to the user's library. */
53
+ async addToLibrary(uri) {
54
+ const state = (await ExpoSpotifySDKModule.userAddToLibrary(uri).catch(rethrowAsUserError));
55
+ notifyLibraryState(state);
56
+ },
57
+ /** Removes a track or album URI from the user's library. */
58
+ async removeFromLibrary(uri) {
59
+ const state = (await ExpoSpotifySDKModule.userRemoveFromLibrary(uri).catch(rethrowAsUserError));
60
+ notifyLibraryState(state);
61
+ },
62
+ /**
63
+ * Subscribes to user-scoped events.
64
+ * Supported event: `"capabilitiesChange"`.
65
+ */
66
+ addListener(event, listener) {
67
+ return ExpoSpotifySDKModule.addListener("onCapabilitiesChange", listener);
68
+ },
69
+ /**
70
+ * Subscribes to library state changes for a specific URI.
71
+ *
72
+ * There is no native push stream for per-URI library state updates on all
73
+ * platforms, so this listener is updated whenever library mutations are
74
+ * performed via this SDK and can be manually seeded by calling
75
+ * `User.getLibraryState(uri)` before subscribing.
76
+ */
77
+ addLibraryStateListener(uri, listener) {
78
+ const key = String(uri);
79
+ let set = libraryListeners.get(key);
80
+ if (!set) {
81
+ set = new Set();
82
+ libraryListeners.set(key, set);
83
+ }
84
+ set.add(listener);
85
+ return {
86
+ remove() {
87
+ const current = libraryListeners.get(key);
88
+ if (!current)
89
+ return;
90
+ current.delete(listener);
91
+ if (current.size === 0)
92
+ libraryListeners.delete(key);
93
+ },
94
+ };
95
+ },
96
+ };
97
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/user/index.ts"],"names":[],"mappings":"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","sourcesContent":["export type { UserErrorCode } from \"./error\";\nexport { UserError } from \"./error\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nimport { EventSubscription } from \"expo-modules-core\";\n\nimport ExpoSpotifySDKModule from \"../ExpoSpotifySDKModule\";\nimport type { SpotifyURI as SpotifyURIType } from \"../uri\";\nimport { UserError, UserErrorCode } from \"./error\";\n\n/** Capabilities of the current user in Spotify's App Remote context. */\nexport interface Capabilities {\n /** `true` for Premium users with on-demand playback capability. */\n canPlayOnDemand: boolean;\n}\n\n/** Library save state for a specific album/track URI. */\nexport interface LibraryState {\n uri: string;\n isAdded: boolean;\n canAdd: boolean;\n}\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\nconst VALID_USER_CODES = new Set<UserErrorCode>([\n \"NOT_CONNECTED\",\n \"CONNECTION_LOST\",\n \"INVALID_URI\",\n \"OPERATION_NOT_ALLOWED\",\n \"UNKNOWN\",\n]);\n\nconst CAUSE_SEPARATOR = \"→ Caused by: \";\n\nfunction unwrapReason(message: string): string {\n const idx = message.lastIndexOf(CAUSE_SEPARATOR);\n return idx === -1 ? message : message.slice(idx + CAUSE_SEPARATOR.length);\n}\n\nfunction rethrowAsUserError(err: unknown): never {\n if (err instanceof UserError) throw err;\n if (err instanceof Error) {\n const reason = unwrapReason(err.message);\n const maybeCode = (err as Error & { code?: string }).code;\n if (maybeCode && VALID_USER_CODES.has(maybeCode as UserErrorCode)) {\n throw new UserError(maybeCode as UserErrorCode, reason);\n }\n throw new UserError(\"UNKNOWN\", reason);\n }\n throw new UserError(\"UNKNOWN\", String(err));\n}\n\ntype LibraryListener = (state: LibraryState) => void;\nconst libraryListeners = new Map<string, Set<LibraryListener>>();\n\nfunction notifyLibraryState(state: LibraryState) {\n const set = libraryListeners.get(state.uri);\n if (!set) return;\n set.forEach((listener) => listener(state));\n}\n\n/**\n * Spotify User namespace. Capabilities and library-state operations.\n * Requires `AppRemote.connect()` to be resolved before any call.\n */\nexport const User = {\n /** Returns the current user's capabilities. */\n getCapabilities(): Promise<Capabilities> {\n return ExpoSpotifySDKModule.userGetCapabilities().catch(rethrowAsUserError);\n },\n\n /** Returns the current library state for a track or album URI. */\n getLibraryState(uri: SpotifyURIType): Promise<LibraryState> {\n return ExpoSpotifySDKModule.userGetLibraryState(uri).catch(rethrowAsUserError);\n },\n\n /** Adds a track or album URI to the user's library. */\n async addToLibrary(uri: SpotifyURIType): Promise<void> {\n const state = (await ExpoSpotifySDKModule.userAddToLibrary(uri).catch(\n rethrowAsUserError,\n )) as LibraryState;\n notifyLibraryState(state);\n },\n\n /** Removes a track or album URI from the user's library. */\n async removeFromLibrary(uri: SpotifyURIType): Promise<void> {\n const state = (await ExpoSpotifySDKModule.userRemoveFromLibrary(uri).catch(\n rethrowAsUserError,\n )) as LibraryState;\n notifyLibraryState(state);\n },\n\n /**\n * Subscribes to user-scoped events.\n * Supported event: `\"capabilitiesChange\"`.\n */\n addListener(\n event: \"capabilitiesChange\",\n listener: (event: Capabilities) => void,\n ): EventSubscription {\n return ExpoSpotifySDKModule.addListener(\n \"onCapabilitiesChange\",\n listener as (event: unknown) => void,\n ) as EventSubscription;\n },\n\n /**\n * Subscribes to library state changes for a specific URI.\n *\n * There is no native push stream for per-URI library state updates on all\n * platforms, so this listener is updated whenever library mutations are\n * performed via this SDK and can be manually seeded by calling\n * `User.getLibraryState(uri)` before subscribing.\n */\n addLibraryStateListener(\n uri: SpotifyURIType,\n listener: (state: LibraryState) => void,\n ): EventSubscription {\n const key = String(uri);\n let set = libraryListeners.get(key);\n if (!set) {\n set = new Set();\n libraryListeners.set(key, set);\n }\n set.add(listener);\n return {\n remove() {\n const current = libraryListeners.get(key);\n if (!current) return;\n current.delete(listener);\n if (current.size === 0) libraryListeners.delete(key);\n },\n } as EventSubscription;\n },\n} as const;\n"]}
@@ -1,13 +1,47 @@
1
1
  import ExpoModulesCore
2
2
  import SpotifyiOS
3
3
 
4
- private let SDK_VERSION = "0.8.0" // x-release-please-version
4
+ private let SDK_VERSION = "1.0.0" // x-release-please-version
5
5
  private let EVENT_SESSION_CHANGE = "onSessionChange"
6
+ private let EVENT_CONNECTION_STATE_CHANGE = "onConnectionStateChange"
7
+ private let EVENT_CONNECTION_ERROR = "onConnectionError"
8
+ private let EVENT_PLAYER_STATE_CHANGE = "onPlayerStateChange"
9
+ private let EVENT_CAPABILITIES_CHANGE = "onCapabilitiesChange"
6
10
 
7
11
  public class ExpoSpotifySDKModule: Module {
8
12
  public func definition() -> ModuleDefinition {
9
13
  Name("ExpoSpotifySDK")
10
- Events(EVENT_SESSION_CHANGE)
14
+
15
+ Events(
16
+ EVENT_SESSION_CHANGE,
17
+ EVENT_CONNECTION_STATE_CHANGE,
18
+ EVENT_CONNECTION_ERROR,
19
+ EVENT_PLAYER_STATE_CHANGE,
20
+ EVENT_CAPABILITIES_CHANGE
21
+ )
22
+
23
+ // Wire up App Remote coordinator event callbacks once the module is alive.
24
+ OnCreate {
25
+ Task {
26
+ guard let coordinator = SpotifyAppRemoteCoordinator.shared else { return }
27
+ await coordinator.setEventHandlers(
28
+ onConnectionStateChange: { [weak self] state in
29
+ self?.sendEvent(EVENT_CONNECTION_STATE_CHANGE, ["state": state])
30
+ },
31
+ onConnectionError: { [weak self] code, message in
32
+ self?.sendEvent(EVENT_CONNECTION_ERROR, ["code": code, "message": message])
33
+ },
34
+ onPlayerStateChange: { [weak self] stateMap in
35
+ self?.sendEvent(EVENT_PLAYER_STATE_CHANGE, stateMap)
36
+ },
37
+ onCapabilitiesChange: { [weak self] capabilities in
38
+ self?.sendEvent(EVENT_CAPABILITIES_CHANGE, capabilities)
39
+ }
40
+ )
41
+ }
42
+ }
43
+
44
+ // MARK: — Auth
11
45
 
12
46
  Function("isAvailable") { () -> Bool in
13
47
  SpotifyAuthCoordinator.shared?.isSpotifyAppInstalled() ?? false
@@ -43,7 +77,7 @@ public class ExpoSpotifySDKModule: Module {
43
77
  "type": "didFail",
44
78
  "error": ["code": error.code, "message": error.message],
45
79
  ])
46
- throw GenericException("\(error.code): \(error.message)")
80
+ throw SpotifyAuthException(error)
47
81
  }
48
82
  }
49
83
 
@@ -79,17 +113,203 @@ public class ExpoSpotifySDKModule: Module {
79
113
  "type": "didFail",
80
114
  "error": ["code": error.code, "message": error.message],
81
115
  ])
82
- throw GenericException("\(error.code): \(error.message)")
116
+ throw SpotifyAuthException(error)
83
117
  } catch let error as SpotifyRefreshError {
84
118
  self.sendEvent(EVENT_SESSION_CHANGE, [
85
119
  "type": "didFail",
86
120
  "error": ["code": error.code, "message": error.message],
87
121
  ])
88
- throw GenericException("\(error.code): \(error.message)")
122
+ throw SpotifyRefreshException(error)
123
+ }
124
+ }
125
+
126
+ // MARK: — App Remote
127
+
128
+ AsyncFunction("appRemoteConnect") { (accessToken: String) async throws -> Void in
129
+ guard let coordinator = SpotifyAppRemoteCoordinator.shared else {
130
+ throw AppRemoteException(AppRemoteError.connectionFailed(
131
+ "Missing `ExpoSpotifySDK` configuration in Info.plist."
132
+ ))
133
+ }
134
+ do {
135
+ try await coordinator.connect(accessToken: accessToken)
136
+ } catch let error as AppRemoteError {
137
+ throw AppRemoteException(error)
138
+ }
139
+ }
140
+
141
+ AsyncFunction("appRemoteDisconnect") { () async -> Void in
142
+ await SpotifyAppRemoteCoordinator.shared?.disconnect()
143
+ }
144
+
145
+ Function("appRemoteIsConnected") { () -> Bool in
146
+ SpotifyAppRemoteCoordinator.shared?.isConnected() ?? false
147
+ }
148
+
149
+ AsyncFunction("appRemoteGetConnectionState") { () async -> String in
150
+ await SpotifyAppRemoteCoordinator.shared?.getConnectionState() ?? "disconnected"
151
+ }
152
+
153
+ // MARK: — Player
154
+
155
+ AsyncFunction("playerPlay") { (uri: String) async throws -> Void in
156
+ guard let coordinator = SpotifyAppRemoteCoordinator.shared else {
157
+ throw PlayerException(NativePlayerError.notConnected("Player.play: missing configuration"))
158
+ }
159
+ do { try await coordinator.playerPlay(uri: uri) } catch let e as NativePlayerError { throw PlayerException(e) }
160
+ }
161
+
162
+ AsyncFunction("playerPause") { () async throws -> Void in
163
+ guard let coordinator = SpotifyAppRemoteCoordinator.shared else {
164
+ throw PlayerException(NativePlayerError.notConnected("Player.pause: missing configuration"))
165
+ }
166
+ do { try await coordinator.playerPause() } catch let e as NativePlayerError { throw PlayerException(e) }
167
+ }
168
+
169
+ AsyncFunction("playerResume") { () async throws -> Void in
170
+ guard let coordinator = SpotifyAppRemoteCoordinator.shared else {
171
+ throw PlayerException(NativePlayerError.notConnected("Player.resume: missing configuration"))
172
+ }
173
+ do { try await coordinator.playerResume() } catch let e as NativePlayerError { throw PlayerException(e) }
174
+ }
175
+
176
+ AsyncFunction("playerSkipNext") { () async throws -> Void in
177
+ guard let coordinator = SpotifyAppRemoteCoordinator.shared else {
178
+ throw PlayerException(NativePlayerError.notConnected("Player.skipNext: missing configuration"))
179
+ }
180
+ do { try await coordinator.playerSkipNext() } catch let e as NativePlayerError { throw PlayerException(e) }
181
+ }
182
+
183
+ AsyncFunction("playerSkipPrevious") { () async throws -> Void in
184
+ guard let coordinator = SpotifyAppRemoteCoordinator.shared else {
185
+ throw PlayerException(NativePlayerError.notConnected("Player.skipPrevious: missing configuration"))
186
+ }
187
+ do { try await coordinator.playerSkipPrevious() } catch let e as NativePlayerError { throw PlayerException(e) }
188
+ }
189
+
190
+ AsyncFunction("playerSeekTo") { (positionMs: Int) async throws -> Void in
191
+ guard let coordinator = SpotifyAppRemoteCoordinator.shared else {
192
+ throw PlayerException(NativePlayerError.notConnected("Player.seekTo: missing configuration"))
193
+ }
194
+ do { try await coordinator.playerSeekTo(positionMs: positionMs) } catch let e as NativePlayerError { throw PlayerException(e) }
195
+ }
196
+
197
+ AsyncFunction("playerSetShuffle") { (enabled: Bool) async throws -> Void in
198
+ guard let coordinator = SpotifyAppRemoteCoordinator.shared else {
199
+ throw PlayerException(NativePlayerError.notConnected("Player.setShuffle: missing configuration"))
200
+ }
201
+ do { try await coordinator.playerSetShuffle(enabled: enabled) } catch let e as NativePlayerError { throw PlayerException(e) }
202
+ }
203
+
204
+ AsyncFunction("playerSetRepeatMode") { (mode: Int) async throws -> Void in
205
+ guard let coordinator = SpotifyAppRemoteCoordinator.shared else {
206
+ throw PlayerException(NativePlayerError.notConnected("Player.setRepeatMode: missing configuration"))
207
+ }
208
+ do { try await coordinator.playerSetRepeatMode(mode: mode) } catch let e as NativePlayerError { throw PlayerException(e) }
209
+ }
210
+
211
+ AsyncFunction("playerSetPodcastPlaybackSpeed") { (value: Double) async throws -> Void in
212
+ guard let coordinator = SpotifyAppRemoteCoordinator.shared else {
213
+ throw PlayerException(NativePlayerError.notConnected("Player.setPodcastPlaybackSpeed: missing configuration"))
214
+ }
215
+ do {
216
+ try await coordinator.playerSetPodcastPlaybackSpeed(value: Float(value))
217
+ } catch let e as NativePlayerError {
218
+ throw PlayerException(e)
219
+ }
220
+ }
221
+
222
+ AsyncFunction("playerQueue") { (uri: String) async throws -> Void in
223
+ guard let coordinator = SpotifyAppRemoteCoordinator.shared else {
224
+ throw PlayerException(NativePlayerError.notConnected("Player.queue: missing configuration"))
225
+ }
226
+ do { try await coordinator.playerQueue(uri: uri) } catch let e as NativePlayerError { throw PlayerException(e) }
227
+ }
228
+
229
+ AsyncFunction("playerGetPlayerState") { () async throws -> [String: Any] in
230
+ guard let coordinator = SpotifyAppRemoteCoordinator.shared else {
231
+ throw PlayerException(NativePlayerError.notConnected("Player.getPlayerState: missing configuration"))
232
+ }
233
+ do { return try await coordinator.playerGetPlayerState() } catch let e as NativePlayerError { throw PlayerException(e) }
234
+ }
235
+
236
+ AsyncFunction("playerGetCrossfadeState") { () async throws -> [String: Any] in
237
+ guard let coordinator = SpotifyAppRemoteCoordinator.shared else {
238
+ throw PlayerException(NativePlayerError.notConnected("Player.getCrossfadeState: missing configuration"))
239
+ }
240
+ do { return try await coordinator.playerGetCrossfadeState() } catch let e as NativePlayerError { throw PlayerException(e) }
241
+ }
242
+
243
+ // MARK: — User
244
+
245
+ AsyncFunction("userGetCapabilities") { () async throws -> [String: Any] in
246
+ guard let coordinator = SpotifyAppRemoteCoordinator.shared else {
247
+ throw UserException(NativeUserError.notConnected("User.getCapabilities: missing configuration"))
248
+ }
249
+ do { return try await coordinator.userGetCapabilities() } catch let e as NativeUserError { throw UserException(e) }
250
+ }
251
+
252
+ AsyncFunction("userGetLibraryState") { (uri: String) async throws -> [String: Any] in
253
+ guard let coordinator = SpotifyAppRemoteCoordinator.shared else {
254
+ throw UserException(NativeUserError.notConnected("User.getLibraryState: missing configuration"))
255
+ }
256
+ do { return try await coordinator.userGetLibraryState(uri: uri) } catch let e as NativeUserError { throw UserException(e) }
257
+ }
258
+
259
+ AsyncFunction("userAddToLibrary") { (uri: String) async throws -> [String: Any] in
260
+ guard let coordinator = SpotifyAppRemoteCoordinator.shared else {
261
+ throw UserException(NativeUserError.notConnected("User.addToLibrary: missing configuration"))
262
+ }
263
+ do { return try await coordinator.userAddToLibrary(uri: uri) } catch let e as NativeUserError { throw UserException(e) }
264
+ }
265
+
266
+ AsyncFunction("userRemoveFromLibrary") { (uri: String) async throws -> [String: Any] in
267
+ guard let coordinator = SpotifyAppRemoteCoordinator.shared else {
268
+ throw UserException(NativeUserError.notConnected("User.removeFromLibrary: missing configuration"))
269
+ }
270
+ do { return try await coordinator.userRemoveFromLibrary(uri: uri) } catch let e as NativeUserError { throw UserException(e) }
271
+ }
272
+
273
+ // MARK: — Content
274
+
275
+ AsyncFunction("contentGetRecommendedContentItems") { (type: String) async throws -> [[String: Any]] in
276
+ guard let coordinator = SpotifyAppRemoteCoordinator.shared else {
277
+ throw ContentException(NativeContentError.notConnected("Content.getRecommendedContentItems: missing configuration"))
278
+ }
279
+ do {
280
+ return try await coordinator.contentGetRecommendedContentItems(type: type)
281
+ } catch let e as NativeContentError {
282
+ throw ContentException(e)
283
+ }
284
+ }
285
+
286
+ AsyncFunction("contentGetChildren") { (item: [String: Any]) async throws -> [[String: Any]] in
287
+ guard let coordinator = SpotifyAppRemoteCoordinator.shared else {
288
+ throw ContentException(NativeContentError.notConnected("Content.getChildren: missing configuration"))
289
+ }
290
+ do {
291
+ return try await coordinator.contentGetChildren(itemMap: item)
292
+ } catch let e as NativeContentError {
293
+ throw ContentException(e)
294
+ }
295
+ }
296
+
297
+ // MARK: — Images
298
+
299
+ AsyncFunction("imagesLoad") { (imageIdentifier: String, size: String) async throws -> [String: Any] in
300
+ guard let coordinator = SpotifyAppRemoteCoordinator.shared else {
301
+ throw ImagesException(NativeImagesError.notConnected("Images.load: missing configuration"))
302
+ }
303
+ do {
304
+ return try await coordinator.imagesLoad(imageIdentifier: imageIdentifier, size: size)
305
+ } catch let e as NativeImagesError {
306
+ throw ImagesException(e)
89
307
  }
90
308
  }
91
309
  }
92
310
 
311
+ // MARK: — Helpers
312
+
93
313
  private func sessionToMap(_ session: SPTSession) -> [String: Any?] {
94
314
  return [
95
315
  "accessToken": session.accessToken,
@@ -100,6 +320,8 @@ public class ExpoSpotifySDKModule: Module {
100
320
  }
101
321
  }
102
322
 
323
+ // MARK: — Record types
324
+
103
325
  struct AuthenticateOptions: Record {
104
326
  @Field var scopes: [String] = []
105
327
  @Field var tokenSwapURL: String? = nil