@feedmepos/auth
v1.7.0
Published
Browser SDK for FeedMe's global authentication service. Embeds the account service's login/one-tap pages via iframe and exposes the signed-in session to your web app through a small static API (`FeedMeAuth`).
Keywords
Readme
@feedmepos/auth
Browser SDK for FeedMe's global authentication service. Embeds the account
service's login/one-tap pages via iframe and exposes the signed-in session to
your web app through a small static API (FeedMeAuth).
Install
npm install @feedmepos/authQuick start
import { FeedMeAuth } from "@feedmepos/auth";
// Point at the environment (dev shown; omit/`false` for prod).
FeedMeAuth.setBaseUrl(true); // https://account.feedmedev.cc
FeedMeAuth.initialize({
clientId: "<your client id>",
loginMode: "iframe", // or "new_window"
locale: "en-US",
});
FeedMeAuth.onAuthStateChanged((user, token) => {
// user is null when signed out
});
// Show the login UI (one-tap → iframe/new-window login)
FeedMeAuth.signIn();Merchant-owned login (SSO)
Some merchants run their own customer login. When an app serves such a merchant, pass its business id:
FeedMeAuth.initialize({
clientId: "<your client id>",
businessId: "<merchant business id>",
loginMode: "new_window", // or "redirect"
});
FeedMeAuth.signIn();signIn() then asks account-service (POST /api/sso/resolve) whether that
business has a login provider registered:
- no provider — everything behaves exactly as it does without
businessId: one-tap, the FeedMe phone form, saved accounts. - a provider — the customer goes to the merchant's identity provider, comes back with a one-time ticket, and the SDK signs them into that merchant's tenant session. They have no FeedMe account and no FeedMe profile.
A tenant session lives in its own Firebase app instance, so it never signs out or overwrites a FeedMe session in the same browser: a customer can be signed into the FeedMe app and a merchant miniprogram at the same time.
Which session the API reports
While a merchant session is active, currentUser(), token,
refreshToken(), getCustomToken() and onAuthStateChanged all report that
session, and signOut() clears only it. With businessId set, the FeedMe
session is reported only once /api/sso/resolve has answered that the
business has no provider. Until then, and whenever the business cannot be
classified (the call fails, is refused for your origin, or answers something
unexpected), currentUser() resolves null and token is null, unless a
merchant session is active, in which case they report that. In the same
cases refreshToken() and getCustomToken() reject rather than fall back to
the FeedMe session, and signOut() resolves without signing the FeedMe
session out. An account-service that predates the route counts as "no
provider", so the FeedMe session keeps working there.
A merchant session restored from this browser's storage is used only when the business runs its own login. After a business is switched back to FeedMe login, a restored merchant session is ignored and the FeedMe session is reported instead. If the classification fails, the restored merchant session is still used: it belongs to the customer, so it is safe to show.
| Method | Purpose |
|---|---|
| activeTenantId() | Identity Platform tenant of the active merchant session, else null. |
| isTenantSessionActive() | Whether a merchant session is signed in and in use. |
| isMerchantLogin() | Whether this app's business runs its own login. |
| merchantName() | Merchant display name, once resolved. |
| lastSsoError() | Last merchant sign-in failure code this page load, else null. |
| signInTenantSession({ customToken, tenantId, businessId }) | Sign in a merchant session directly (native shells that run the flow themselves). Rejects, leaving no session active, when the sign-in fails or returns no user or token. |
Login modes
| loginMode | Merchant SSO | Notes |
|---|---|---|
| "new_window" | ✅ | Popup; the login page hands the session back to the SDK. Default. |
| "redirect" | ✅ | Navigates the whole page and returns to location.href. The only mode that is reliable inside native webviews, where popups are often blocked or opened in a separate browser. |
| "iframe" | ❌ | The merchant's consent page and IdP are not guaranteed to be frameable. When the business runs its own login, signIn() logs a warning and opens a "new_window" popup. A business without a provider keeps iframe mode. |
In "redirect" mode the app is returned to with a one-time sso_ticket
parameter (never a token). initialize() exchanges it, signs the session in
and strips the parameter from the URL, so no application code is needed beyond
setting the mode. A failure comes back as sso_error and is readable through
lastSsoError().
What is switched off for merchant sessions
- one-tap and saved accounts — those are FeedMe accounts.
verifyOtp()— FeedMe phone verification; it rejects with"FeedMeAuth: phone verification is not available for merchant sessions".
Manage profile for merchant sessions
openManageProfile() / getManageProfileUrl() open the customer's merchant
membership page (/manage/account/merchant?business_id=…) instead of the
FeedMe account page. It shows the customer's merchant profile (name and phone
from the merchant sign-in, email, birthday and gender from account-service; the
CRM is not read) and lets the customer edit name, email, birthday and gender
there. The phone stays read-only, and the email is saved as contact data
without a verification code, so it is not a sign-in method. Your app needs no
write route of its own.
The page gets its session from your app: it asks the SDK (its
window.opener) for the merchant session, and the SDK answers only for its
own businessId. The URL carries your app's origin as host_origin, since
the page cannot read its opener's origin itself. So open it with
openManageProfile() from a user gesture.
A copied link or a severed opener shows an error screen, never the FeedMe
login page.
Both calls throw when there is no merchant session to show:
"FeedMeAuth: manage profile needs a signed-in merchant session": the business runs its own login and no customer is signed in."FeedMeAuth: manage profile is not available until the business login mode is known": called before/api/sso/resolveanswered. Wait forcurrentUser()or the firstonAuthStateChangedcallback first.
The id token of a merchant session carries phone_number exactly as a FeedMe
one does, plus businessId and firebase.tenant, so ordering and CRM need no
change beyond sending the customer's businessId when they look users up.
Opening the account-management page
openManageProfile() opens the user's FeedMe account page in a new tab:
FeedMeAuth.openManageProfile();
// or build the URL yourself:
const url = FeedMeAuth.getManageProfileUrl();How the session reaches the new tab
The new tab needs the current session to show the right account. As of
1.5.0 the SDK hands that session over directly: the URL carries an
opener=1 flag, the page asks its window.opener (your app) for a token via
postMessage, and the SDK answers with a freshly minted custom token. The tab
signs in with it — no shared cookies or storage involved.
This matters because when your app and the account service are on different
domains (e.g. app.example.com → account.feedmedev.cc), browsers that
block third-party cookies or partition iframe storage — Safari, all iOS
browsers, Firefox, Chrome incognito — prevent the tab from recovering the
session on its own. The postMessage hand-off is not subject to those
restrictions, so it works across every browser and domain. Where the hand-off
can't run, the SDK falls back to the previous cookie/storage recovery, so
same-domain setups keep working exactly as before.
⚠️ COOP requirement (cross-domain hand-off)
The hand-off relies on the new tab keeping a live window.opener reference
back to your app. If your app's pages send a strict
Cross-Origin-Opener-Policy header, the browser severs that reference for
cross-origin popups and the hand-off cannot happen — the tab then falls back
to cookie/storage recovery, which fails on the privacy-strict browsers above,
sending the user to a login prompt even though they're already signed in.
If you serve Cross-Origin-Opener-Policy on the pages that call
openManageProfile(), use a value that preserves the opener link:
| Header value | Manage-profile hand-off |
|---|---|
| (no COOP header) | ✅ works |
| same-origin-allow-popups | ✅ works |
| same-origin | ❌ opener severed — hand-off disabled |
| unsafe-none | ✅ works |
same-origin-allow-popups is the standard choice for apps that open OAuth-style
popups, and is the recommended setting if you need COOP at all.
Also do not pass noopener when opening the page yourself (window.open(url,
"_blank", "noopener") or rel="noopener" links) — it nulls window.opener
for the same reason. openManageProfile() opens correctly on your behalf.
API
| Method | Purpose |
|---|---|
| setBaseUrl(isDev \| url) | Select the account-service environment. |
| initialize(options) | Boot the SDK (background iframe, listeners). |
| signIn(opts?) | Show the login UI. |
| signOut() | Sign out; resolves once the session is cleared. |
| onAuthStateChanged(cb) | Subscribe to (user, token) changes; returns an unsubscribe fn. |
| onLoginComplete(cb) | Fires when a login flow completes. |
| currentUser() | Resolve the current FeedMeUser \| null. |
| token / refreshToken() | Read / force-refresh the id token. |
| getCustomToken(idToken?) | Mint a custom token for the session. |
| getManageProfileUrl() / openManageProfile() | Account-management page URL / open it in a new tab. |
| verifyOtp({ phone, tokenType }) | Run the standalone OTP verification flow. |
| activeTenantId() | Tenant of the active merchant session (see Merchant-owned login). |
| isMerchantLogin() | Whether this app's business runs its own login. |
| lastSsoError() | Last merchant sign-in failure code, if any. |
