Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,8 +225,8 @@ export class AccountManager {

/**
* Ask the server what it offers and what it thinks of the session. Run when the
* dialog opens, before a sign-in, and at activation only when a session is stored;
* a session the server no longer knows is dropped.
* dialog opens, before and after a sign-in, after a sign-out, and at activation only
* when a session is stored; a session the server no longer knows is dropped.
*/
async connect(): Promise<void> {
const info = await this.client.info();
Expand Down Expand Up @@ -338,7 +338,8 @@ export class AccountManager {
this.view = m.account;
this.changed();
finish(() => resolve(m.account!));
void this.refresh().catch(() => {});
// (What the server offers depends on who asks: the answer from before the sign-in was a guest's.)
void this.connect().then(() => this.refresh()).catch(() => {});
});
const watch = setInterval(() => { if (popup.closed) finish(() => reject(new ScmjsError("aborted", "the sign-in window was closed."))); }, 500);
const limit = setTimeout(() => { finish(() => { try { popup.close(); } catch { /* gone */ } reject(new ScmjsError("network", t("the sign-in did not finish in time."))); }); }, this.deps.signInTimeoutMs ?? 5 * 60_000);
Expand All @@ -351,6 +352,8 @@ export class AccountManager {
this.view = null;
this.ledgerRows = [];
this.changed();
// (Ask again as a guest, so nothing the account was offered stays on show.)
await this.connect().catch(() => {});
}

/** The payment page for a pack, in a new tab. */
Expand Down
8 changes: 5 additions & 3 deletions dist/plugin.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@scm-js/plugin-scmjs-dev",
"version": "1.37.0",
"version": "1.37.1",
"private": true,
"description": "scmjs.dev — a plugin for the scmJS StarCraft map editor: your account, map storage with revisions, and the AI features (scenarios, maps, triggers, an assistant) on one free trial and one sign-in.",
"author": "Jeany",
Expand Down
2 changes: 1 addition & 1 deletion plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "scmjs.dev",
"id": "scmjs-dev",
"version": "1.37.0",
"version": "1.37.1",
"description": "Your scmjs.dev account in the editor. Keep maps on your account with revisions and notes, and share the open map so others can edit it with you at the same time. Where the server offers them, AI tools can build, lay out and review a map with you.",
"author": "Jeany",
"homepage": "https://github.com/scm-js/plugin-scmjs-dev",
Expand Down
36 changes: 34 additions & 2 deletions tests/plugin.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { describe, expect, it } from "vitest";
import { describe, expect, it, vi } from "vitest";
import { AccountManager, memoryStore } from "../account";
import { describeError, fileNameFrom, formatBytes, ScmjsClient, ScmjsError } from "../client";
import { describeMeta, matchesSearch, metaOf } from "../maps";
Expand Down Expand Up @@ -169,14 +169,46 @@ describe("the account manager", () => {
expect(account.casts).toBeNull();
});

it("asks again what is offered after a sign-in and a sign-out, so a guest's answer does not stick", async () => {
const accounts = { providers: [{ id: "discord", name: "Discord" }], trial: false, trialUsd: 0, signupUsd: 0, weeklyUsd: 0, packs: [], accountUrl: "https://api.example/account", maps: true };
const admin = view({ role: "admin", unlimited: true, balanceUsd: 0, weeklyUsd: 0, creditUsd: 0 });
let session: string | null = null;
const signedIn = (init: RequestInit) => session !== null && ((init.headers ?? {}) as Record<string, string>).Authorization === `Bearer ${session}`;
const { fetchImpl } = fakeFetch({
// The server casts for a signed-in admin only.
"GET /v1/info": (init) => json({ name: "Test", version: "1", accounts, speech: true, recipes: [{ name: "cast", enabled: signedIn(init), model: "m" }], caller: signedIn(init) ? { kind: "user", remaining: {}, account: admin } : { kind: "anonymous", remaining: {} } }),
"POST /v1/auth/start": () => json({ url: "https://discord.com/oauth2/authorize?state=abc" }),
"POST /v1/auth/logout": () => { session = null; return json({ ok: true }); },
"GET /v1/account": () => json({ account: admin, ledger: [] }),
});
const store = memoryStore();
let handler: ((e: { origin: string; data: unknown }) => void) | null = null;
const popup = { closed: false, location: { href: "" }, close() { this.closed = true; } };
const account = new AccountManager(store, new ScmjsClient(() => ({ serverUrl: "https://api.example", session: store.get().session }), fetchImpl), {
openPopup: () => popup, origin: () => "https://editor.example", listen: (h) => { handler = h; return () => { handler = null; }; },
});
// The Account window was opened before signing in: a guest's answer.
await account.connect();
expect(account.casts).toEqual({ on: false, voiced: true });
const signing = account.signIn();
await new Promise((r) => setTimeout(r, 0));
session = "sess_admin";
handler!({ origin: "https://api.example", data: { type: "scmjs-ai-auth", session: "sess_admin", account: admin } });
await signing;
await vi.waitFor(() => expect(account.casts).toEqual({ on: true, voiced: true }));
expect(account.kind()).toBe("account");
await account.signOut();
expect(account.casts).toEqual({ on: false, voiced: true });
});

it("offers the AI only where the server says the role may call it, and keeps the answer for the next start", async () => {
let signedIn = false;
const accounts = { providers: [{ id: "discord", name: "Discord" }], trial: false, trialUsd: 0, signupUsd: 0, weeklyUsd: 0, packs: [], accountUrl: "https://api.example/account", maps: true, ai: false };
const admin = view({ kind: "account", name: "Boss", role: "admin", unlimited: true, balanceUsd: 0, weeklyUsd: 0, creditUsd: 0, providers: ["discord"], ai: true });
const { fetchImpl } = fakeFetch({
"GET /v1/info": () => json({ name: "Test", version: "1", accounts, caller: signedIn ? { kind: "user", remaining: {}, account: admin } : { kind: "anonymous", remaining: {} } }),
"GET /v1/account": () => json({ account: admin, ledger: [] }),
"POST /v1/auth/logout": () => json({ ok: true }),
"POST /v1/auth/logout": () => { signedIn = false; return json({ ok: true }); },
});
const store = memoryStore({ aiOffered: true });
const account = new AccountManager(store, new ScmjsClient(() => ({ serverUrl: "https://api.example", session: store.get().session }), fetchImpl));
Expand Down
Loading