Files
accounted/contexts/CompanyContext.tsx
T
Mattsson 99c94d467a fix(white-label): back-to-clients link points at the byra cockpit's home domain (#1973)
* fix(white-label): back-to-clients link points at the byra cockpit's home domain

A byra member working a company homed on another host (e.g. a pre-byra
company on canonical) got a relative /clients on the wrong host instead
of their white-label cockpit. resolveCockpitHref mirrors WL-14's home
rule: relative when the current host is the cockpit's home (brand domain,
or canonical for a brandless byra), else an absolute URL there. Cross-
host links render a plain <a> with a 'Hanteras via' hint; the hop lands
on the brand host's login (per-host sessions, WL-01) and WL-14 then
lands on /clients.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(white-label): show the cross-host cockpit hint as visible text

CodeRabbit: title-only hints never surface on touch devices, so the
expanded-sidebar and mobile external back-links now render the
'Hanteras via {domain}' line as small muted text under the label
(same pattern as the switcher's foreign entries). Also corrects the
comments claiming the no-company branch never renders the back-link:
it can, on its cockpit/settings surfaces, where the relative fallback
matches pre-change behavior.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-27 13:44:23 +02:00

100 lines
3.4 KiB
TypeScript

'use client'
import { createContext, useContext } from 'react'
import type { Company, CompanyRole, Team } from '@/types'
import type { CapabilityKey } from '@/lib/entitlements/keys'
import type { EntitlementState } from '@/lib/entitlements/has-capability'
/** The user's byrå team membership (teams.kind = 'byra'), when any (WL-08). */
export interface ByraTeamRef {
id: string
name: string
role: 'owner' | 'admin' | 'member'
/**
* Where "Tillbaka till klienter" points: '/clients' when the current host
* is the byrå cockpit's home domain, else an absolute URL to that home
* (resolveCockpitHref). Optional: the no-company layout branch renders
* before brands resolve and leaves it unset; consumers fall back to
* '/clients', which on that branch's cockpit/settings surfaces matches the
* pre-cockpitHref behavior exactly.
*/
cockpitHref?: string
}
/**
* A membership company homed on ANOTHER host (the home-domain rule, WL-01):
* shown as a non-clickable "Hanteras via <domain>" signpost entry.
*/
export interface ForeignCompanyEntry {
id: string
name: string
domain: string
}
interface CompanyContextValue {
company: Company | null
role: CompanyRole | null
/**
* Memberships homed on the CURRENT host (home-domain rule): the switcher
* offers only these. Companies homed elsewhere are in foreignCompanies.
*/
companies: { company: Company; role: CompanyRole }[]
isTeamMember: boolean
team: Team | null
/** Byrå team membership; null for everyone outside a byrå (cockpit gate). */
byraTeam?: ByraTeamRef | null
/** Companies homed on another domain, for the "Hanteras via" section. */
foreignCompanies?: ForeignCompanyEntry[]
isSandbox: boolean
/** PAID capability keys the active company currently holds (entitled + enabled). */
capabilities: CapabilityKey[]
/**
* Trial expiry while the trial is the company's only source of paid access;
* null when paying/comped or after the trial lapsed. Drives the countdown
* touchpoint in the sidebar.
*/
trialEndsAt: string | null
/**
* Paid-lifecycle state (trial / trial_expired / lapsed_subscription /
* paid / none). 'none' in the company:null shells. Drives the expired-trial
* touchpoint and dialog.
*/
entitlementState: EntitlementState
/** When the lapsed trial ran out; set only while entitlementState is 'trial_expired'. */
trialExpiredAt: string | null
}
const CompanyContext = createContext<CompanyContextValue | null>(null)
export function CompanyProvider({
children,
value,
}: {
children: React.ReactNode
value: CompanyContextValue
}) {
return <CompanyContext.Provider value={value}>{children}</CompanyContext.Provider>
}
export function useCompany() {
const ctx = useContext(CompanyContext)
if (!ctx) throw new Error('useCompany must be used within CompanyProvider')
return ctx
}
export function useCompanyOptional() {
return useContext(CompanyContext)
}
/**
* Whether the active company holds a given paid capability. Controls UI
* affordances only: the server gate (lib/entitlements) is the real enforcement.
* Fail-open when rendered outside a CompanyProvider (e.g. standalone dialogs):
* the server still blocks; this only decides whether to show/disable/upsell.
*/
export function useCapability(key: CapabilityKey): boolean {
const ctx = useContext(CompanyContext)
if (!ctx) return true
return ctx.capabilities.includes(key)
}