P15a: hosting a shared session requires a signed-in GM
- The Host control now checks cloudUsername(): when the GM isn't signed in it shows "🔒 Sign in to host" (→ Settings) instead of "📡 Host". Players still join as guests. - Realtime e2e: the GM marks itself signed in before hosting. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -12,8 +12,9 @@ test('GM hosts a session; a player on another device sees live combat', async ({
|
|||||||
const gmCtx = await browser.newContext();
|
const gmCtx = await browser.newContext();
|
||||||
const playerCtx = await browser.newContext();
|
const playerCtx = await browser.newContext();
|
||||||
|
|
||||||
// GM: load the sample campaign and host a session.
|
// GM: load the sample campaign and host a session (hosting requires a signed-in GM).
|
||||||
const gm = await fresh(gmCtx);
|
const gm = await fresh(gmCtx);
|
||||||
|
await gm.evaluate(() => localStorage.setItem('ttrpg-cloud-user', 'GM'));
|
||||||
gm.on('dialog', (d) => d.dismiss()); // dismiss the optional-password prompt
|
gm.on('dialog', (d) => d.dismiss()); // dismiss the optional-password prompt
|
||||||
await gm.getByLabel('Settings').click();
|
await gm.getByLabel('Settings').click();
|
||||||
await gm.getByRole('button', { name: 'Load sample campaign' }).click();
|
await gm.getByRole('button', { name: 'Load sample campaign' }).click();
|
||||||
|
|||||||
@@ -12,8 +12,9 @@ test('a player claims a seat and manages their character; edits round-trip to th
|
|||||||
const gmCtx = await browser.newContext();
|
const gmCtx = await browser.newContext();
|
||||||
const playerCtx = await browser.newContext();
|
const playerCtx = await browser.newContext();
|
||||||
|
|
||||||
// GM: sample campaign + host.
|
// GM: sample campaign + host (hosting requires a signed-in GM).
|
||||||
const gm = await fresh(gmCtx);
|
const gm = await fresh(gmCtx);
|
||||||
|
await gm.evaluate(() => localStorage.setItem('ttrpg-cloud-user', 'GM'));
|
||||||
gm.on('dialog', (d) => d.dismiss()); // optional-password prompt
|
gm.on('dialog', (d) => d.dismiss()); // optional-password prompt
|
||||||
await gm.getByLabel('Settings').click();
|
await gm.getByLabel('Settings').click();
|
||||||
await gm.getByRole('button', { name: 'Load sample campaign' }).click();
|
await gm.getByRole('button', { name: 'Load sample campaign' }).click();
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
import { useNavigate } from '@tanstack/react-router';
|
||||||
import { useLiveQuery } from 'dexie-react-hooks';
|
import { useLiveQuery } from 'dexie-react-hooks';
|
||||||
import { useSessionStore, type SeatRequest } from '@/stores/sessionStore';
|
import { useSessionStore, type SeatRequest } from '@/stores/sessionStore';
|
||||||
import { hostSession, stopSession, grantSeat } from '@/lib/sync/wsSync';
|
import { hostSession, stopSession, grantSeat } from '@/lib/sync/wsSync';
|
||||||
import { charactersRepo } from '@/lib/db/repositories';
|
import { charactersRepo } from '@/lib/db/repositories';
|
||||||
|
import { cloudUsername } from '@/lib/cloud/client';
|
||||||
import { useActiveCampaign } from '@/features/campaigns/hooks';
|
import { useActiveCampaign } from '@/features/campaigns/hooks';
|
||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/Button';
|
||||||
|
|
||||||
/** Header control: GM hosts a live session, shares the code, and approves seats. */
|
/** Header control: GM hosts a live session, shares the code, and approves seats. */
|
||||||
export function SessionControl() {
|
export function SessionControl() {
|
||||||
|
const navigate = useNavigate();
|
||||||
const campaign = useActiveCampaign();
|
const campaign = useActiveCampaign();
|
||||||
const role = useSessionStore((s) => s.role);
|
const role = useSessionStore((s) => s.role);
|
||||||
const status = useSessionStore((s) => s.status);
|
const status = useSessionStore((s) => s.status);
|
||||||
@@ -20,6 +23,14 @@ export function SessionControl() {
|
|||||||
|
|
||||||
if (role === 'off') {
|
if (role === 'off') {
|
||||||
if (!campaign) return null;
|
if (!campaign) return null;
|
||||||
|
// Hosting a shared session requires the GM to be signed in to an account.
|
||||||
|
if (!cloudUsername()) {
|
||||||
|
return (
|
||||||
|
<Button size="sm" variant="ghost" title="Sign in to host a shared session" onClick={() => void navigate({ to: '/settings' })}>
|
||||||
|
🔒 Sign in to host
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<Button size="sm" variant="ghost" title="Host a live session for players"
|
<Button size="sm" variant="ghost" title="Host a live session for players"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user