= { card: "#3b82f6", safe_wallet: "#10b981", ridentity: "#7c3aed", metamask: "#f6851b", unconfigured: "#64748b" };
+ const inputStyle = `width:100%;padding:10px 12px;border:1px solid var(--rflows-modal-border, #334155);border-radius:8px;font-size:14px;box-sizing:border-box;background:var(--rs-bg-surface, #1e293b);color:var(--rs-text-primary, #e2e8f0)`;
+
+ const modal = document.createElement("div");
+ modal.className = "source-modal";
+
+ const renderMethodDetail = () => {
+ const detailEl = modal.querySelector(".spm-method-detail") as HTMLElement;
+ if (!detailEl) return;
+ if (sd.sourceType === "card") {
+ detailEl.innerHTML = ``;
+ } else if (sd.sourceType === "metamask") {
+ const addr = sd.walletAddress ? `Connected: ${sd.walletAddress}
` : "";
+ detailEl.innerHTML = `${addr}`;
+ } else if (sd.sourceType === "ridentity") {
+ const session = getSession();
+ detailEl.innerHTML = `${session ? `Linked as ${session.claims.username || session.claims.sub}` : "Not signed in"}
`;
+ } else {
+ detailEl.innerHTML = "";
+ }
+ // Re-attach detail listeners
+ modal.querySelector("[data-spm-action='fund-card']")?.addEventListener("click", (e: Event) => {
+ e.stopPropagation();
+ this.openUserOnRamp(nodeId).catch((err) => console.error("[UserOnRamp] Error:", err));
+ });
+ modal.querySelector("[data-spm-action='connect-metamask']")?.addEventListener("click", async (e: Event) => {
+ e.stopPropagation();
+ await this.connectMetaMask(nodeId);
+ renderMethodDetail();
+ });
+ };
+
+ const updateMethodBtns = () => {
+ modal.querySelectorAll(".spm-method-btn").forEach((btn) => {
+ const type = (btn as HTMLElement).dataset.spmType || "";
+ btn.classList.toggle("spm-method-btn--active", type === sd.sourceType);
+ });
+ };
+
+ // Build allocation display
+ let allocHtml = "";
+ if (sd.targetAllocations && sd.targetAllocations.length > 0) {
+ const barSegs = sd.targetAllocations.map(a =>
+ ``
+ ).join("");
+ const labels = sd.targetAllocations.map(a =>
+ `${this.esc(this.getNodeLabel(a.targetId))} — ${a.percentage}%
`
+ ).join("");
+ allocHtml = `
+
Allocations
+
${barSegs}
+
${labels}
+
`;
+ }
+
+ modal.innerHTML = `
+
+
+
Configure Source
+
+
+
Payment Method
+
+
+
+
+
+
+ ${allocHtml}
+
+
+
+
+
+
`;
+
+ document.body.appendChild(modal);
+ renderMethodDetail();
+
+ // Live field updates
+ const labelInput = modal.querySelector(".spm-label-input") as HTMLInputElement;
+ const amountInput = modal.querySelector(".spm-amount-input") as HTMLInputElement;
+
+ const applyChanges = () => {
+ this.redrawNodeOnly(node);
+ this.redrawEdges();
+ this.scheduleSave();
+ };
+
+ labelInput.addEventListener("input", () => { sd.label = labelInput.value; applyChanges(); });
+ amountInput.addEventListener("input", () => { sd.flowRate = parseFloat(amountInput.value) || 0; applyChanges(); });
+
+ // Method selection
+ modal.querySelectorAll(".spm-method-btn").forEach((btn) => {
+ btn.addEventListener("click", (e: Event) => {
+ e.stopPropagation();
+ sd.sourceType = ((btn as HTMLElement).dataset.spmType || "unconfigured") as SourceNodeData["sourceType"];
+ updateMethodBtns();
+ renderMethodDetail();
+ applyChanges();
+ });
+ });
+
+ // Close / Delete
+ const closeModal = () => {
+ this.sourceModalNodeId = null;
+ modal.remove();
+ };
+
+ modal.querySelector(".spm-close-btn")!.addEventListener("click", closeModal);
+ modal.querySelector(".spm-backdrop")!.addEventListener("click", closeModal);
+ modal.addEventListener("keydown", (e: KeyboardEvent) => {
+ if (e.key === "Escape") closeModal();
+ });
+
+ modal.querySelector(".spm-delete-btn")!.addEventListener("click", () => {
+ closeModal();
+ this.nodes = this.nodes.filter((nn) => nn.id !== nodeId);
+ this.drawCanvasContent();
+ this.scheduleSave();
+ });
+
+ labelInput.focus();
+ }
+
+ private async connectMetaMask(nodeId: string) {
+ const node = this.nodes.find((n) => n.id === nodeId);
+ if (!node || node.type !== "source") return;
+ const sd = node.data as SourceNodeData;
+ const ethereum = (window as any).ethereum;
+ if (!ethereum) {
+ alert("MetaMask not detected. Please install the MetaMask browser extension.");
+ return;
+ }
+ try {
+ const accounts: string[] = await ethereum.request({ method: "eth_requestAccounts" });
+ const chainId: string = await ethereum.request({ method: "eth_chainId" });
+ sd.walletAddress = accounts[0];
+ sd.chainId = parseInt(chainId, 16);
+ this.redrawNodeOnly(node);
+ this.redrawEdges();
+ this.scheduleSave();
+ } catch (err) {
+ console.error("[MetaMask] Connection failed:", err);
+ }
+ }
+
/**
* Open on-ramp widget. Coinbase blocks iframing (CSP frame-ancestors),
* so we open it in a popup window. Transak allows iframing.
@@ -4036,7 +4244,7 @@ class FolkFlowsApp extends HTMLElement {
if (!node || node.type !== "source") return;
const d = node.data as SourceNodeData;
- const icons: Record = { card: "\u{1F4B3}", safe_wallet: "\u{1F512}", ridentity: "\u{1F464}", unconfigured: "\u{2699}" };
+ const icons: Record = { card: "\u{1F4B3}", safe_wallet: "\u{1F512}", ridentity: "\u{1F464}", metamask: "\u{1F98A}", unconfigured: "\u{2699}" };
const labels: Record = { card: "Card", safe_wallet: "Safe / Wallet", ridentity: "rIdentity", unconfigured: "Configure" };
let configHtml = "";
diff --git a/modules/rflows/lib/types.ts b/modules/rflows/lib/types.ts
index f7270f6..619bce4 100644
--- a/modules/rflows/lib/types.ts
+++ b/modules/rflows/lib/types.ts
@@ -90,7 +90,7 @@ export interface OutcomeNodeData {
export interface SourceNodeData {
label: string;
flowRate: number;
- sourceType: "card" | "safe_wallet" | "ridentity" | "unconfigured";
+ sourceType: "card" | "safe_wallet" | "ridentity" | "metamask" | "unconfigured";
targetAllocations: SourceAllocation[];
walletAddress?: string;
chainId?: number;