@phantomstudios/ae-music-react
v0.1.0
Published
Headless React bindings for Appreciation Engine + Spotify + Apple Music flows.
Readme
@phantomstudios/ae-music-react
Headless React helpers for Appreciation Engine + Spotify + Apple Music. The package loads or coordinates external scripts, exposes auth state and actions, and stays out of your UI.
For workspace context (examples, why sample reference campaign IDs appear in .env.example, redirect behaviour, coordinating return URLs / allowlists with RCA/Sony, and the presave vs AE OAuth-only example UIs), read README.md at the monorepo root (Campaign-Utils/README.md when you clone this workspace).
Install
From npm (public @phantomstudios package):
npm install @phantomstudios/ae-music-reactPin a semver range in package.json (e.g. ^0.1.0).
What you get
AEMusicProvider— AE lifecycle, optional Apple Music bootstrap, persistence hooks.useAEMusic— status, user,connectSpotify,connectAppleMusic, tokens.AEMusicConnector— render-prop composition without nesting custom hooks.createServiceAuthAttributes—data-ae-register-linkhelper for AE-driven links.buildAECrmPayload/submitFormEncoded— optional Sony-style CRM form POST helpers.
Basic usage
import { AEMusicProvider, useAEMusic } from "@phantomstudios/ae-music-react";
const config = {
ae: {
scriptUrl:
"https://sme.theappreciationengine.com/framework/js/492?segment=YOUR_SEGMENT",
externalScript: false, // set true if Next.js (or your app) injects the script
scopes: {
spotify:
"playlist-modify-private playlist-modify-public user-follow-modify user-library-read user-read-email user-top-read",
},
authWindow: false, // true if you rely on AE popup communication
},
spotify: { serviceName: "spotify" },
appleMusic: {
enabled: true,
config: {
/* SMEAppleMusic constructor JSON from your campaign */
},
defaultMailingLists: ["YOUR_MAILING_LIST_ID"],
},
};
function ConnectView() {
const {
aeStatus,
isLibraryReady,
connectSpotify,
connectAppleMusic,
getSpotifyToken,
} = useAEMusic();
return (
<div>
<p>AE status: {aeStatus}</p>
<button
type="button"
disabled={!isLibraryReady}
onClick={() => connectSpotify({ openIn: "same-window" })}
>
Connect Spotify
</button>
<button
type="button"
onClick={() => void connectAppleMusic({ email: "[email protected]" })}
>
Connect Apple Music
</button>
<pre>{getSpotifyToken() ?? "No Spotify token yet"}</pre>
</div>
);
}
export function App() {
return (
<AEMusicProvider config={config}>
<ConnectView />
</AEMusicProvider>
);
}Use disabled rules that match your flow: if you send users to a Sony presave URL without waiting for AE, you may enable the button even when isLibraryReady is false. The full presave + return orchestration (hostname-aware split, new-tab bootstrap, auth-bridge return) ships as a subpath — see Presave + return helper below.
Next.js notes
- Load the AE script with
next/scriptin the root layout and setae.externalScript: trueso only one script tag exists. - Client components that branch on
window/ hostname (presave vs AE on localhost) should defer that logic until after mount to avoid hydration mismatches—seeexamples/next-showcase.
Spotify window mode
connectSpotify({ openIn: "same-window" });
connectSpotify({ openIn: "new-window" });Render-prop pattern
import { AEMusicConnector } from "@phantomstudios/ae-music-react";
export function AnyDesignSystemComponent() {
return (
<AEMusicConnector>
{({ connectSpotify, spotifyAuthAttributes, isLibraryReady }) => (
<button
type="button"
disabled={!isLibraryReady}
onClick={() => connectSpotify()}
{...spotifyAuthAttributes}
>
Sign in with Spotify
</button>
)}
</AEMusicConnector>
);
}Presave + return helper
Subpath import @phantomstudios/ae-music-react/spotify-flow exposes
usePresaveSpotifyFlow, a React hook that wires the presave URL redirect,
hostname-aware AE vs presave split, new-tab presave bootstrap, and the
auth-bridge return (cross-tab localStorage handshake) into a single
handleSpotifyConnect() callback.
"use client";
import { useAEMusic } from "@phantomstudios/ae-music-react";
import {
isSpotifyConnectEnabled,
usePresaveSpotifyFlow,
} from "@phantomstudios/ae-music-react/spotify-flow";
function PresaveButton() {
const { isLibraryReady, isAuthenticated, connectSpotify, getSpotifyToken } =
useAEMusic();
const {
handleSpotifyConnect,
effectiveAuthenticated,
effectiveSpotifyToken,
} = usePresaveSpotifyFlow({
connectSpotify,
getSpotifyToken,
isAuthenticated,
presaveUrl: process.env.NEXT_PUBLIC_SPOTIFY_PRESAVE_URL,
authMode: process.env.NEXT_PUBLIC_SPOTIFY_AUTH_MODE, // "auto" | "presave" | "ae"
windowModeDefault: process.env.NEXT_PUBLIC_SPOTIFY_WINDOW_MODE, // "same-window" | "new-window"
});
const enabled = isSpotifyConnectEnabled({
isLibraryReady,
presaveUrl: process.env.NEXT_PUBLIC_SPOTIFY_PRESAVE_URL,
authMode: process.env.NEXT_PUBLIC_SPOTIFY_AUTH_MODE,
});
if (effectiveAuthenticated) return <p>Connected.</p>;
return (
<button type="button" disabled={!enabled} onClick={handleSpotifyConnect}>
Connect with Spotify
</button>
);
}| Mode | Behaviour |
| --------- | ------------------------------------------------------------------------- |
| presave | Always navigate to presaveUrl (full-page or new-window bootstrap). |
| ae | Always call AE trigger.authenticate via connectSpotify. |
| auto | Presave when not on localhost, AE when on localhost. |
Window modes — same-window uses window.location.assign(presaveUrl);
new-window opens a popup at /?_ae_auth=spotify which then redirects to
presaveUrl so the opener tab can return to its stored URL.
Optional CRM helper
import { buildAECrmPayload, submitFormEncoded } from "@phantomstudios/ae-music-react";
const payload = buildAECrmPayload({
user,
form: "YOUR_FORM_ID",
defaultMailingList: "YOUR_LIST_ID",
segmentId: "YOUR_SEGMENT",
brandId: "YOUR_BRAND",
activities: '{"actions":{"presave":YOUR_ACTION_ID}}',
});
await submitFormEncoded({
url: "https://subs.sonymusicfans.com/submit",
payload,
});