@the-portland-company/shell 0.55.0 → 0.56.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/dist/app-chrome.cjs +90 -17
- package/dist/app-chrome.cjs.map +1 -1
- package/dist/app-chrome.d.cts +13 -0
- package/dist/app-chrome.d.ts +13 -0
- package/dist/app-chrome.js +75 -3
- package/dist/app-chrome.js.map +1 -1
- package/dist/{chunk-K5GWJSHE.js → chunk-MR6TLHEE.js} +18 -17
- package/dist/chunk-MR6TLHEE.js.map +1 -0
- package/dist/native.cjs +16 -15
- package/dist/native.cjs.map +1 -1
- package/dist/native.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-K5GWJSHE.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,25 @@
|
|
|
1
|
+
## 0.56.0 — 2026-09-30
|
|
2
|
+
|
|
3
|
+
### Fixed — mode continuity for dual-mode apps (a2p)
|
|
4
|
+
|
|
5
|
+
a2p (Specs/A2P) is the SMS/MMS delivery engine for BOTH Relationship Mode and
|
|
6
|
+
Campaign Mode, but it was registered `mode: 'campaign'` only, so the
|
|
7
|
+
Relationship tree's "SMS/MMS Blast" row sent users into a2p and the whole left
|
|
8
|
+
nav flipped to the Campaign tree + "Campaign Mode" pill — a jarring identity
|
|
9
|
+
switch for a user who never left Relationship Mode.
|
|
10
|
+
|
|
11
|
+
`POLITOGY_APPS[app]` now supports an optional `dual: { modes, defaultMode }`.
|
|
12
|
+
a2p declares `dual: { modes: ['relationship', 'campaign'], defaultMode:
|
|
13
|
+
'campaign' }`; its EFFECTIVE mode is resolved per-visit by the new
|
|
14
|
+
`resolveActiveMode` (`src/mode-preference.ts`): `?mode=` query param → the new
|
|
15
|
+
cross-subdomain `politogy_active_mode` cookie → the app's own default. Every
|
|
16
|
+
pinned app and core writes that cookie with its own mode, so arriving at a2p
|
|
17
|
+
after using Relationship Mode elsewhere resolves to Relationship even without
|
|
18
|
+
an explicit `?mode=` link. The Relationship tree's "SMS/MMS Blast" row now
|
|
19
|
+
links straight to a2p with `?mode=relationship` (the dead
|
|
20
|
+
`/relationship/sms-mms-blast/*` core routes are removed); the Campaign tree's
|
|
21
|
+
existing A2P row is unchanged.
|
|
22
|
+
|
|
1
23
|
## 0.55.0 — 2026-09-30
|
|
2
24
|
|
|
3
25
|
### Fixed — org pill body no longer takes the Mode accent (`./native`)
|
package/dist/app-chrome.cjs
CHANGED
|
@@ -1494,11 +1494,6 @@ var APP_EMAIL_CHILDREN = [
|
|
|
1494
1494
|
{ label: "Deliverability", icon: "activity", href: `${EMAIL_APP}/deliverability` }
|
|
1495
1495
|
];
|
|
1496
1496
|
var EMAIL_V1_LEGACY = { label: "Email V1 (legacy)", icon: "clock", path: "/relationship/email/dashboard" };
|
|
1497
|
-
var SMS_CHILDREN = [
|
|
1498
|
-
{ label: "Dashboard", icon: "grid", path: "/relationship/sms-mms-blast/dashboard" },
|
|
1499
|
-
{ label: "Compose & Send", icon: "send", path: "/relationship/sms-mms-blast/compose-and-send" },
|
|
1500
|
-
{ label: "Conversations", icon: "messageSquare", path: "/relationship/sms-mms-blast/conversations" }
|
|
1501
|
-
];
|
|
1502
1497
|
var CTA_CHILDREN = [
|
|
1503
1498
|
{ label: "Dashboard", icon: "grid", path: "/relationship/cta-database/dashboard" },
|
|
1504
1499
|
{ label: "Create & Edit", icon: "edit", path: "/relationship/cta-creator/create-and-edit" },
|
|
@@ -1579,6 +1574,14 @@ function standardPolitogyNav(opts) {
|
|
|
1579
1574
|
href: withOrg(abs(c.path)),
|
|
1580
1575
|
children: c.children ? leaves(c.children) : void 0
|
|
1581
1576
|
}));
|
|
1577
|
+
const appLeaves = (origin, children, modeParam) => {
|
|
1578
|
+
const base2 = origin.replace(/\/+$/, "");
|
|
1579
|
+
return children.map((c) => {
|
|
1580
|
+
const url = c.path ? `${base2}/${c.path}` : `${base2}/`;
|
|
1581
|
+
const withMode = modeParam ? `${url}${url.includes("?") ? "&" : "?"}mode=${modeParam}` : url;
|
|
1582
|
+
return { label: c.label, icon: icon(c.icon), href: withOrg(withMode) };
|
|
1583
|
+
});
|
|
1584
|
+
};
|
|
1582
1585
|
const buildRelationship = () => {
|
|
1583
1586
|
const contacts = opts.contactsItem ? {
|
|
1584
1587
|
label: "Contacts",
|
|
@@ -1607,11 +1610,17 @@ function standardPolitogyNav(opts) {
|
|
|
1607
1610
|
...leaves([EMAIL_V1_LEGACY])
|
|
1608
1611
|
]
|
|
1609
1612
|
},
|
|
1613
|
+
// SMS/MMS Blast IS a2p (Specs/A2P) — the standalone delivery engine
|
|
1614
|
+
// shared with Campaign Mode, not a core-hosted surface (the old
|
|
1615
|
+
// `/relationship/sms-mms-blast/*` core routes are dead; core no longer
|
|
1616
|
+
// hosts them). Every link carries `mode=relationship` so a2p's nav
|
|
1617
|
+
// resolves the Relationship tree instead of its own Campaign default
|
|
1618
|
+
// (resolveActiveMode, mode-preference.ts).
|
|
1610
1619
|
{
|
|
1611
1620
|
label: "SMS/MMS Blast",
|
|
1612
1621
|
icon: icon("messageSquare"),
|
|
1613
|
-
href: withOrg(
|
|
1614
|
-
children:
|
|
1622
|
+
href: withOrg(`${A2P_ORIGIN}?mode=relationship`),
|
|
1623
|
+
children: appLeaves(A2P_ORIGIN, APP_A2P_CHILDREN, "relationship")
|
|
1615
1624
|
},
|
|
1616
1625
|
// These two carry no top-level href in VRM core (parent row only toggles).
|
|
1617
1626
|
{ label: "Calls to Action", icon: icon("layout"), children: leaves(CTA_CHILDREN) },
|
|
@@ -1652,14 +1661,6 @@ function standardPolitogyNav(opts) {
|
|
|
1652
1661
|
...CAMPAIGN_FEATURE_AVAILABILITY,
|
|
1653
1662
|
...opts.campaignAvailability
|
|
1654
1663
|
};
|
|
1655
|
-
const appLeaves = (origin, children) => {
|
|
1656
|
-
const base2 = origin.replace(/\/+$/, "");
|
|
1657
|
-
return children.map((c) => ({
|
|
1658
|
-
label: c.label,
|
|
1659
|
-
icon: icon(c.icon),
|
|
1660
|
-
href: withOrg(c.path ? `${base2}/${c.path}` : `${base2}/`)
|
|
1661
|
-
}));
|
|
1662
|
-
};
|
|
1663
1664
|
const rows = [
|
|
1664
1665
|
{ label: "Dashboard", icon: icon("home"), href: withOrg(abs("/campaign/dashboard")) },
|
|
1665
1666
|
{ label: "Voter Contact", icon: icon("target"), href: withOrg(abs("/campaign/campaigns")) },
|
|
@@ -3256,6 +3257,58 @@ function watchLogout(onLogout, options = {}) {
|
|
|
3256
3257
|
}
|
|
3257
3258
|
return cleanup;
|
|
3258
3259
|
}
|
|
3260
|
+
|
|
3261
|
+
// src/mode-preference.ts
|
|
3262
|
+
var ACTIVE_MODE_COOKIE = "politogy_active_mode";
|
|
3263
|
+
var VALID_MODES = ["relationship", "campaign", "petition"];
|
|
3264
|
+
function isStandardNavMode(v) {
|
|
3265
|
+
return typeof v === "string" && VALID_MODES.includes(v);
|
|
3266
|
+
}
|
|
3267
|
+
function readActiveModeCookie() {
|
|
3268
|
+
if (typeof document === "undefined") return null;
|
|
3269
|
+
try {
|
|
3270
|
+
const m = document.cookie.match(
|
|
3271
|
+
new RegExp(`(?:^|;\\s*)${ACTIVE_MODE_COOKIE}=([^;]+)`)
|
|
3272
|
+
);
|
|
3273
|
+
const v = m ? decodeURIComponent(m[1]) : null;
|
|
3274
|
+
return isStandardNavMode(v) ? v : null;
|
|
3275
|
+
} catch {
|
|
3276
|
+
return null;
|
|
3277
|
+
}
|
|
3278
|
+
}
|
|
3279
|
+
function writeActiveModeCookie(mode) {
|
|
3280
|
+
if (typeof document === "undefined" || typeof window === "undefined") return;
|
|
3281
|
+
const host = window.location.hostname;
|
|
3282
|
+
const domainAttr = /(^|\.)politogyvrm\.com$/i.test(host) ? "; Domain=.politogyvrm.com" : "";
|
|
3283
|
+
const secure = window.location.protocol === "https:" ? "; Secure" : "";
|
|
3284
|
+
document.cookie = `${ACTIVE_MODE_COOKIE}=${encodeURIComponent(
|
|
3285
|
+
mode
|
|
3286
|
+
)}; Path=/; Max-Age=31536000; SameSite=Lax${domainAttr}${secure}`;
|
|
3287
|
+
}
|
|
3288
|
+
function modeFromSearch(search) {
|
|
3289
|
+
if (!search) return null;
|
|
3290
|
+
try {
|
|
3291
|
+
const params = new URLSearchParams(search.startsWith("?") ? search.slice(1) : search);
|
|
3292
|
+
return params.get("mode");
|
|
3293
|
+
} catch {
|
|
3294
|
+
return null;
|
|
3295
|
+
}
|
|
3296
|
+
}
|
|
3297
|
+
function resolveActiveMode({
|
|
3298
|
+
search,
|
|
3299
|
+
modes,
|
|
3300
|
+
defaultMode
|
|
3301
|
+
}) {
|
|
3302
|
+
const fromQuery = modeFromSearch(search);
|
|
3303
|
+
if (fromQuery && isStandardNavMode(fromQuery) && modes.includes(fromQuery)) {
|
|
3304
|
+
return fromQuery;
|
|
3305
|
+
}
|
|
3306
|
+
const fromCookie = readActiveModeCookie();
|
|
3307
|
+
if (fromCookie && modes.includes(fromCookie)) {
|
|
3308
|
+
return fromCookie;
|
|
3309
|
+
}
|
|
3310
|
+
return defaultMode;
|
|
3311
|
+
}
|
|
3259
3312
|
var BOOT_STAGE_TABLE = [
|
|
3260
3313
|
{ id: "s0_session", weight: 5, critical: true, waitsOn: "Politogy ID session resolution" },
|
|
3261
3314
|
{ id: "s1_org", weight: 10, critical: true, waitsOn: "Chrome API /api/chrome/me + orgs" },
|
|
@@ -3625,7 +3678,17 @@ var POLITOGY_APPS = {
|
|
|
3625
3678
|
// Campaign Mode apps — each scoped to Campaign, so they render the Campaign
|
|
3626
3679
|
// tree and a static Campaign pill. Not yet deployed; hidden from the Campaign
|
|
3627
3680
|
// nav by CAMPAIGN_FEATURE_AVAILABILITY until their launch flips the flag.
|
|
3628
|
-
|
|
3681
|
+
// Dual-mode: a2p is the SMS/MMS delivery engine for BOTH Relationship Mode
|
|
3682
|
+
// and Campaign Mode (Specs/A2P). `mode: 'campaign'` stays the fallback for
|
|
3683
|
+
// anything reading `meta.mode` directly; the effective mode a user sees is
|
|
3684
|
+
// resolved per-visit (see `dual`, resolveActiveMode).
|
|
3685
|
+
a2p: {
|
|
3686
|
+
title: "A2P Messaging",
|
|
3687
|
+
origin: "https://a2p.politogyvrm.com",
|
|
3688
|
+
storagePrefix: "a2p",
|
|
3689
|
+
mode: "campaign",
|
|
3690
|
+
dual: { modes: ["relationship", "campaign"], defaultMode: "campaign" }
|
|
3691
|
+
},
|
|
3629
3692
|
phonebank: { title: "Phone Banking", origin: "https://phonebank.politogyvrm.com", storagePrefix: "phonebank", mode: "campaign" },
|
|
3630
3693
|
canvass: { title: "Field Canvassing", origin: "https://canvass.politogyvrm.com", storagePrefix: "canvass", mode: "campaign" },
|
|
3631
3694
|
signs: { title: "Sign Mapping", origin: "https://signs.politogyvrm.com", storagePrefix: "signs", mode: "campaign" }
|
|
@@ -3762,9 +3825,19 @@ function FrameBody({
|
|
|
3762
3825
|
});
|
|
3763
3826
|
const userMenu = core?.userMenu ?? derivedUserMenu;
|
|
3764
3827
|
const footer = useChromeFooter(buildMeta ?? {});
|
|
3765
|
-
const
|
|
3828
|
+
const resolvedDualMode = meta.dual ? resolveActiveMode({
|
|
3829
|
+
search: search ?? (typeof window !== "undefined" ? window.location.search : void 0),
|
|
3830
|
+
modes: meta.dual.modes,
|
|
3831
|
+
defaultMode: meta.dual.defaultMode
|
|
3832
|
+
}) : void 0;
|
|
3833
|
+
const appMode = resolvedDualMode ?? meta.mode;
|
|
3766
3834
|
const derivedMode = useChromeMode(appMode ? { pinnedModeId: appMode } : void 0);
|
|
3767
3835
|
const mode = core?.mode ?? derivedMode;
|
|
3836
|
+
const coreLiveMode = app === CORE_APP ? pathname.startsWith("/campaign") ? "campaign" : pathname.startsWith("/relationship") ? "relationship" : void 0 : void 0;
|
|
3837
|
+
const cookieMode = coreLiveMode ?? (meta.dual ? void 0 : meta.mode);
|
|
3838
|
+
react.useEffect(() => {
|
|
3839
|
+
if (cookieMode) writeActiveModeCookie(cookieMode);
|
|
3840
|
+
}, [cookieMode]);
|
|
3768
3841
|
const coreOrgs = chrome?.orgs ?? [];
|
|
3769
3842
|
const activeCoreOrg = coreOrgs.find((o) => o.id === chrome?.orgId) ?? null;
|
|
3770
3843
|
const navDisplay = chrome?.navDisplay ?? "both";
|