99c94d467a
* 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>
100 lines
3.4 KiB
TypeScript
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)
|
|
}
|