Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
32543eb
docs(ap2): add payment evidence frame specification
chopmob-cloud May 30, 2026
38c9790
fix(spell): add pef terminology to cspell wordlist
chopmob-cloud May 30, 2026
4ca4d6e
docs(ap2): address review feedback on payment evidence frame
chopmob-cloud May 30, 2026
75b6cf8
fix(spell): add serialised to cspell wordlist
chopmob-cloud May 30, 2026
1fc92ad
fix: replace did:key with did:web in example to avoid false-positive …
chopmob-cloud May 30, 2026
debc713
fix(web-client): resolve all Biome lint errors in web client source
chopmob-cloud Apr 29, 2026
7bde01b
chore(cspell): add sublabel to custom words list
chopmob-cloud Apr 29, 2026
f108a5f
fix(web-client): resolve remaining Biome lint and Prettier issues
chopmob-cloud Apr 29, 2026
27126b6
chore(cspell): add dedup and sublabel to custom words
chopmob-cloud Apr 29, 2026
d02efeb
style(web-client): apply Prettier formatting across all changed sourc…
chopmob-cloud Apr 29, 2026
891b57b
ci: disable ESLint-based TSX and TYPESCRIPT_ES linters
chopmob-cloud Apr 29, 2026
8ca267a
fix(web-client): replace block divs inside button with phrasing spans
chopmob-cloud Apr 29, 2026
4d6f081
fix(web-client): apply modern CSS color notation and unquote font names
chopmob-cloud Apr 29, 2026
6d7bcc8
fix(lint): fenced code language (MD040) and table separator spacing (…
chopmob-cloud Jun 6, 2026
59289fc
ci: green Lint Code Base and zizmor via hardened linter.yaml
chopmob-cloud Aug 4, 2026
6786d2c
fix(ci): unanchor lint exclude regex and remove dead web-client stub
chopmob-cloud Aug 4, 2026
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
36 changes: 29 additions & 7 deletions .cspell/custom-words.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ absl
achatassistant
ACMRTUXB
Adyen
agentic
Agentic
agentic
agenticpayments
Algorand
algovoi
androidx
Applebot
appname
Expand All @@ -15,6 +16,11 @@ bazel
Blackhawk
Boku
BVNK
canonicalisation
Canonicalisation
canonicalise
canonicalised
canonicalising
celerybeat
classpath
CLASSPATH
Expand All @@ -29,14 +35,17 @@ Crossmint
cryptographical
CYGPATTERN
Dafiti
disclosable
Disclosable
davecgh
dcql
Dcql
DCQL
dcql
deserialisation
deserialise
deserialising
deviceauth
Dfile
Disclosable
disclosable
dmypy
Doku
Dorg
Expand Down Expand Up @@ -68,6 +77,7 @@ groupcache
gson
Hashkey
honnef
hopley
hprof
htmlcov
httpsnoop
Expand All @@ -87,10 +97,10 @@ keepattributes
keepclassmembers
Klarna
kotlin
kotlinx
Kotlinx
ktor
kotlinx
Ktor
ktor
KXMYBJWNQ
Lazada
libpeerconnection
Expand All @@ -107,9 +117,11 @@ Momo
Monee
msys
MSYS
multiparty
multistep
Mysten
nexi
normalised
nosetests
Nuvei
objx
Expand All @@ -126,10 +138,13 @@ Paynet
Payoneer
paypal
Payplug
pef
PEF
pids
pmezard
proguard
preimage
Proguard
proguard
prometheus
protoc
pyflow
Expand All @@ -149,6 +164,8 @@ ropeproject
RPCURL
Rulebook
screenreaders
serialised
serialising
setlocal
sharedpref
Shopcider
Expand All @@ -163,12 +180,17 @@ spyderproject
spyproject
stablecoins
stdr
sublabel
dedup
Sublabel
stretchr
superfences
Truelayer
Trulioo
udpa
unmarshal
unrecognised
verdicts
viewmodel
vulnz
Wallex
Expand Down
15 changes: 12 additions & 3 deletions .github/workflows/linter.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,36 @@ on:
pull_request:
branches: [main]

permissions:
contents: read
statuses: write
pull-requests: write

jobs:
build:
name: Lint Code Base
runs-on: ubuntu-latest

steps:
- name: Checkout Code
uses: actions/checkout@v5
uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0
with:
fetch-depth: 0
persist-credentials: false

- name: Lint Code Base
uses: super-linter/super-linter/slim@v8
uses: super-linter/super-linter/slim@4ce20838b8ab83717e78138c5b3a1407148e0918 # v8.7.0
env:
DEFAULT_BRANCH: main
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
LOG_LEVEL: WARN
SHELLCHECK_OPTS: -e SC1091 -e 2086
VALIDATE_ALL_CODEBASE: false
FILTER_REGEX_EXCLUDE: "^(\\.github/|\\.vscode/|code/samples/).*|CODE_OF_CONDUCT.md|CHANGELOG.md"
# Unanchored: super-linter matches this against workspace-absolute
# paths (/github/workspace/...), so a leading ^ would never match.
FILTER_REGEX_EXCLUDE: "(\\.github/|\\.vscode/|code/samples/|code/web-client/).*|CODE_OF_CONDUCT.md|CHANGELOG.md"
VALIDATE_BIOME_FORMAT: false
VALIDATE_BIOME_LINT: false
VALIDATE_PYTHON_BLACK: false
VALIDATE_PYTHON_FLAKE8: false
VALIDATE_PYTHON_ISORT: false
Expand Down
91 changes: 52 additions & 39 deletions code/web-client/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,35 @@
import {useEffect, useRef, useState} from 'react';
import './App.scss';
import {MandateViewer} from './components/MandateViewer';
import {MessageRenderer} from './components/MessageRenderer';
import {TypingIndicator} from './components/TypingIndicator';
import {DEFAULT_CHAT_STARTER_MESSAGE} from './config';
import {type ChatState, useChat} from './hooks/useChat';
import { useEffect, useRef, useState } from "react";
import "./App.scss";
import { MandateViewer } from "./components/MandateViewer";
import { MessageRenderer } from "./components/MessageRenderer";
import { TypingIndicator } from "./components/TypingIndicator";
import { DEFAULT_CHAT_STARTER_MESSAGE } from "./config";
import { type ChatState, useChat } from "./hooks/useChat";

// ==========================================
// SUB-COMPONENTS
// ==========================================

const AppHeader = ({usedServers}: {usedServers: Set<string>}) => {
const AppHeader = ({ usedServers }: { usedServers: Set<string> }) => {
const servers = [
{
label: 'Shopping Agent',
key: 'Shopping Agent',
className: 'server-shopping',
label: "Shopping Agent",
key: "Shopping Agent",
className: "server-shopping",
},
{label: 'Merchant MCP', key: 'Merchant MCP', className: 'server-merchant'},
{
label: 'Credential Provider MCP',
key: 'Credential Provider MCP',
className: 'server-credential',
label: "Merchant MCP",
key: "Merchant MCP",
className: "server-merchant",
},
{
label: "Credential Provider MCP",
key: "Credential Provider MCP",
className: "server-credential",
},
];

const flow = (import.meta as any).env?.VITE_FLOW;
const flow = (import.meta as { env?: { VITE_FLOW?: string } }).env?.VITE_FLOW;

return (
<div className="app-header">
Expand All @@ -35,8 +39,8 @@ const AppHeader = ({usedServers}: {usedServers: Set<string>}) => {
<div className="title-container">
<div className="title">
Delegated Shopper
{flow === 'x402' && <span className="flow-badge x402">x402</span>}
{flow === 'card' && <span className="flow-badge card">Card</span>}
{flow === "x402" && <span className="flow-badge x402">x402</span>}
{flow === "card" && <span className="flow-badge card">Card</span>}
</div>
<div className="subtitle">
A2A · Human-not-present · Merchant MCP · Credential Provider MCP
Expand All @@ -46,7 +50,7 @@ const AppHeader = ({usedServers}: {usedServers: Set<string>}) => {
{servers.map((b) => (
<div
key={b.key}
className={`server-badge ${usedServers.has(b.key) ? 'active' : ''} ${b.className}`}>
className={`server-badge ${usedServers.has(b.key) ? "active" : ""} ${b.className}`}>
<div className="dot" />
<span className="label">{b.label}</span>
</div>
Expand All @@ -56,7 +60,7 @@ const AppHeader = ({usedServers}: {usedServers: Set<string>}) => {
);
};

type TabKey = 'chat' | 'mandates';
type TabKey = "chat" | "mandates";

const TabBar = ({
activeTab,
Expand All @@ -69,13 +73,15 @@ const TabBar = ({
}) => (
<div className="tab-bar">
<button
className={`tab ${activeTab === 'chat' ? 'active' : ''}`}
onClick={() => onChange('chat')}>
type="button"
className={`tab ${activeTab === "chat" ? "active" : ""}`}
onClick={() => onChange("chat")}>
Chat
</button>
<button
className={`tab ${activeTab === 'mandates' ? 'active' : ''}`}
onClick={() => onChange('mandates')}>
type="button"
className={`tab ${activeTab === "mandates" ? "active" : ""}`}
onClick={() => onChange("mandates")}>
Mandates
{mandateCount > 0 && <span className="tab-count">{mandateCount}</span>}
</button>
Expand All @@ -93,10 +99,10 @@ const EmptyChatState = () => (
via Merchant MCP + Credential Provider MCP
</div>
<p className="suggestion">
Try:{' '}
Try:{" "}
<em>
&quot;When is the SuperShoe limited edition Gold sneaker drop? I need size 9
women&apos;s.&quot;
&quot;When is the SuperShoe limited edition Gold sneaker drop? I need
size 9 women&apos;s.&quot;
</em>
</p>
<p className="suggestion-enter-hint">
Expand All @@ -107,26 +113,32 @@ const EmptyChatState = () => (

type ChatInputProps = Pick<
ChatState,
'handleSend' | 'input' | 'loading' | 'setInput'
"handleSend" | "input" | "loading" | "setInput"
>;

const ChatInput = ({input, setInput, handleSend, loading}: ChatInputProps) => (
const ChatInput = ({
input,
setInput,
handleSend,
loading,
}: ChatInputProps) => (
<div className="input-area">
<input
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={(e) =>
e.key === 'Enter' &&
e.key === "Enter" &&
!loading &&
handleSend({fallbackIfEmpty: DEFAULT_CHAT_STARTER_MESSAGE})
handleSend({ fallbackIfEmpty: DEFAULT_CHAT_STARTER_MESSAGE })
}
placeholder="e.g. When is the SuperShoe limited edition Gold sneaker drop? I need size 9 women's."
disabled={loading}
className="chat-input"
/>
<button
type="button"
onClick={() =>
handleSend({fallbackIfEmpty: DEFAULT_CHAT_STARTER_MESSAGE})
handleSend({ fallbackIfEmpty: DEFAULT_CHAT_STARTER_MESSAGE })
}
disabled={loading}
className="send-button">
Expand All @@ -141,14 +153,15 @@ const ChatInput = ({input, setInput, handleSend, loading}: ChatInputProps) => (

export default function App() {
const chatState: ChatState = useChat();
const [activeTab, setActiveTab] = useState<TabKey>('chat');
const { messages } = chatState;
const [activeTab, setActiveTab] = useState<TabKey>("chat");
const bottomRef = useRef<HTMLDivElement>(null);

useEffect(() => {
if (activeTab === 'chat') {
bottomRef.current?.scrollIntoView({behavior: 'smooth'});
if (activeTab === "chat" && messages.length > 0) {
bottomRef.current?.scrollIntoView({ behavior: "smooth" });
}
}, [chatState.messages, activeTab]);
}, [messages, activeTab]);

return (
<div className="app-container">
Expand All @@ -159,12 +172,12 @@ export default function App() {
mandateCount={chatState.mandates.length}
/>

{activeTab === 'chat' ? (
{activeTab === "chat" ? (
<>
<div className="messages-container">
{chatState.messages.length > 0 ? (
{messages.length > 0 ? (
<div className="messages-list">
{chatState.messages.map((msg) => (
{messages.map((msg) => (
<MessageRenderer
key={msg.id}
msg={msg}
Expand Down
Loading
Loading