Skip to content

Commit

Permalink
Merge branch 'main' into feat/add-permissionless
Browse files Browse the repository at this point in the history
* main:
  fix: store connected dapp
  hotfix: add remove flag to wallet build
  chore update gitignore
  write the key to the database
  update wallet origin
  fix tsconfig
  move wallet to vercel
  add ssr to wallet
  [studio] Add GitHub Action
  hidden frame on away
  remove dead code
  send the credential from the wallet to dapp
  wip create a fallback if no credential exists
  attempt to add display items
  • Loading branch information
jamesmccomish committed Mar 16, 2024
2 parents f6bd8c7 + c8ddad3 commit 4f1e5a9
Show file tree
Hide file tree
Showing 29 changed files with 1,111 additions and 179 deletions.
26 changes: 26 additions & 0 deletions .github/workflows/_studio.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name: Astro Studio

env:
ASTRO_STUDIO_APP_TOKEN: ${{secrets.ASTRO_STUDIO_APP_TOKEN }}

on:
push:
branches:
- main
pull_request:
types: [opened, reopened, synchronize]

jobs:
DB:
permissions:
contents: read
actions: read
pull-requests: write
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- uses: jaid/[email protected]
- uses: withastro/action-studio@main
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
node_modules
**/.firebase
**/.firebase
**/.vercel
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
global.css,1710590178343,42d73ffbdad77a1b36c73186c8ecfedf609c52809da1ae37a2853a0dd10a91a2
favicon.svg,1710590178343,11f7bbcc36bf2f54c31744264fa1b24a0811ed62e4d26d5b18d3b2dec99040a7
_astro/create-payment-button.CINVb1iY.js,1710590178345,db69defc7d4eecb5b29b7fa206694342e57bed69e28954a4622b3c0112fab89a
_astro/hoisted.Dtw3UAUy.js,1710590178344,8f395373b0a398615502bc5d7b4630f3e0a2312777ddfdcf19854284d8dfe4d6
index.html,1710590178343,e287d0e9ce1e596069fe4eaffdf556d2ed9f40ee061e8ea03109b98c704d8172
_astro/index.NEDEFKed.js,1710590178344,c1250d9adcf7af9f08cbad6c8b34267ac3bd193c676424dc5653db6fc9505e90
_astro/utils.CR3g-f2u.js,1710590178344,c953e34917739c011debb7b1c24907478e139d6bc5d139b3d99a2ba45fed48c3
_astro/client.DbokQZWz.js,1710590178345,dfc9d946776d09a6ecd38cb440df7115dce93ec392f2272238dc903ac9f14163
favicon.svg,1710612600596,11f7bbcc36bf2f54c31744264fa1b24a0811ed62e4d26d5b18d3b2dec99040a7
global.css,1710612600596,42d73ffbdad77a1b36c73186c8ecfedf609c52809da1ae37a2853a0dd10a91a2
index.html,1710612600596,4f4375b1d8a51aa512086a8597622b47c35c025491a244381d86050a6de93acd
_astro/hoisted.bU3TEY26.js,1710612600597,12298e4f606a273f1d1d5645e995b3f08bf59eb700b3581f4e2978abb27ab38a
_astro/create-payment-button.CE6C2yQc.js,1710612600598,12a2a4c61d23bc3d87b947c3202cb6a457bf9fa0b7b91087f1bf9cc339644c82
_astro/index.8-_CQqnL.css,1710612600597,e9c06675410edad1a5e1e4185fe3e9c9a0002f94035f81189fe63e262c734447
_astro/index.NEDEFKed.js,1710612600597,c1250d9adcf7af9f08cbad6c8b34267ac3bd193c676424dc5653db6fc9505e90
_astro/constants.Bq4ER0LB.js,1710612600598,15319ae0b9cb6cac4168d89ce7618a53261b798b6ae301b81bdb49ae48ec7f75
_astro/client.DbokQZWz.js,1710612600598,dfc9d946776d09a6ecd38cb440df7115dce93ec392f2272238dc903ac9f14163
8 changes: 1 addition & 7 deletions apps/dapp/.firebase/spc-dapp/hosting/index.html
Original file line number Diff line number Diff line change
@@ -1,7 +1 @@
<!DOCTYPE html><html lang="en"> <head><meta charset="utf-8"><link rel="icon" type="image/svg+xml" href="/favicon.svg"><link rel="stylesheet" href="/global.css"><meta name="viewport" content="width=device-width"><meta name="generator" content="Astro v4.5.4"><title>SPC dApp</title><script type="module" src="/_astro/hoisted.Dtw3UAUy.js"></script></head> <body class="flex flex-col items-center justify-center h-screen"> <main class="flex flex-col items-center justify-evenly min-h-[20vh]"> <!-- <form
id="form"
action={`${paymentOrigin}/`}
class="center"
target="iframe"
method="post"
> --> <h1>SPC dApp</h1> <div id="iframe-container" class="hidden"> <iframe name="iframe" allow="payment https://spc-wallet.web.app"></iframe> </div> <style>astro-island,astro-slot,astro-static-slot{display:contents}</style><script>(()=>{var e=async t=>{await(await t())()};(self.Astro||(self.Astro={})).only=e;window.dispatchEvent(new Event("astro:only"));})();;(()=>{var v=Object.defineProperty;var A=(c,s,a)=>s in c?v(c,s,{enumerable:!0,configurable:!0,writable:!0,value:a}):c[s]=a;var d=(c,s,a)=>(A(c,typeof s!="symbol"?s+"":s,a),a);var u;{let c={0:t=>m(t),1:t=>a(t),2:t=>new RegExp(t),3:t=>new Date(t),4:t=>new Map(a(t)),5:t=>new Set(a(t)),6:t=>BigInt(t),7:t=>new URL(t),8:t=>new Uint8Array(t),9:t=>new Uint16Array(t),10:t=>new Uint32Array(t)},s=t=>{let[e,n]=t;return e in c?c[e](n):void 0},a=t=>t.map(s),m=t=>typeof t!="object"||t===null?t:Object.fromEntries(Object.entries(t).map(([e,n])=>[e,s(n)]));customElements.get("astro-island")||customElements.define("astro-island",(u=class extends HTMLElement{constructor(){super(...arguments);d(this,"Component");d(this,"hydrator");d(this,"hydrate",async()=>{var f;if(!this.hydrator||!this.isConnected)return;let e=(f=this.parentElement)==null?void 0:f.closest("astro-island[ssr]");if(e){e.addEventListener("astro:hydrate",this.hydrate,{once:!0});return}let n=this.querySelectorAll("astro-slot"),r={},l=this.querySelectorAll("template[data-astro-template]");for(let o of l){let i=o.closest(this.tagName);i!=null&&i.isSameNode(this)&&(r[o.getAttribute("data-astro-template")||"default"]=o.innerHTML,o.remove())}for(let o of n){let i=o.closest(this.tagName);i!=null&&i.isSameNode(this)&&(r[o.getAttribute("name")||"default"]=o.innerHTML)}let h;try{h=this.hasAttribute("props")?m(JSON.parse(this.getAttribute("props"))):{}}catch(o){let i=this.getAttribute("component-url")||"<unknown>",b=this.getAttribute("component-export");throw b&&(i+=` (export ${b})`),console.error(`[hydrate] Error parsing props for component ${i}`,this.getAttribute("props"),o),o}let p;await this.hydrator(this)(this.Component,h,r,{client:this.getAttribute("client")}),this.removeAttribute("ssr"),this.dispatchEvent(new CustomEvent("astro:hydrate"))});d(this,"unmount",()=>{this.isConnected||this.dispatchEvent(new CustomEvent("astro:unmount"))})}disconnectedCallback(){document.removeEventListener("astro:after-swap",this.unmount),document.addEventListener("astro:after-swap",this.unmount,{once:!0})}connectedCallback(){if(!this.hasAttribute("await-children")||document.readyState==="interactive"||document.readyState==="complete")this.childrenConnectedCallback();else{let e=()=>{document.removeEventListener("DOMContentLoaded",e),n.disconnect(),this.childrenConnectedCallback()},n=new MutationObserver(()=>{var r;((r=this.lastChild)==null?void 0:r.nodeType)===Node.COMMENT_NODE&&this.lastChild.nodeValue==="astro:end"&&(this.lastChild.remove(),e())});n.observe(this,{childList:!0}),document.addEventListener("DOMContentLoaded",e)}}async childrenConnectedCallback(){let e=this.getAttribute("before-hydration-url");e&&await import(e),this.start()}async start(){let e=JSON.parse(this.getAttribute("opts")),n=this.getAttribute("client");if(Astro[n]===void 0){window.addEventListener(`astro:${n}`,()=>this.start(),{once:!0});return}try{await Astro[n](async()=>{let r=this.getAttribute("renderer-url"),[l,{default:h}]=await Promise.all([import(this.getAttribute("component-url")),r?import(r):()=>()=>{}]),p=this.getAttribute("component-export")||"default";if(!p.includes("."))this.Component=l[p];else{this.Component=l;for(let y of p.split("."))this.Component=this.Component[y]}return this.hydrator=h,this.hydrate},e,this)}catch(r){console.error(`[astro-island] Error hydrating ${this.getAttribute("component-url")}`,r)}}attributeChangedCallback(){this.hydrate()}},d(u,"observedAttributes",["props"]),u))}})();</script><astro-island uid="1iICUQ" component-url="/_astro/create-payment-button.CINVb1iY.js" component-export="CreatePaymentButton" renderer-url="/_astro/client.DbokQZWz.js" props="{}" ssr="" client="only" opts="{&quot;name&quot;:&quot;CreatePaymentButton&quot;,&quot;value&quot;:true}"></astro-island> <!-- </form> --> </main> </body> </html>
<!DOCTYPE html><html lang="en"> <head><meta charset="utf-8"><link rel="icon" type="image/svg+xml" href="/favicon.svg"><link rel="stylesheet" href="/global.css"><meta name="viewport" content="width=device-width"><meta name="generator" content="Astro v4.5.4"><title>SPC dApp</title><link rel="stylesheet" href="/_astro/index.8-_CQqnL.css" /><script type="module" src="/_astro/hoisted.bU3TEY26.js"></script></head> <body class="flex flex-col items-center justify-center h-screen"> <main class="flex flex-col items-center justify-evenly min-h-[20vh]"> <h1>SPC dApp</h1> <dialog id="wallet-iframe-dialog" class="items-center justify-center border rounded-lg py-3 px-4 min-h-[40vh] backdrop:bg-black backdrop:opacity-50 backdrop:backdrop-blur-md backdrop:transition-opacity backdrop:duration-300"> <iframe class="min-h-[40vh] border-0 m-0" name="iframe" allow="payment https://spc-wallet.vercel.app"></iframe> </dialog> <style>astro-island,astro-slot,astro-static-slot{display:contents}</style><script>(()=>{var e=async t=>{await(await t())()};(self.Astro||(self.Astro={})).only=e;window.dispatchEvent(new Event("astro:only"));})();;(()=>{var v=Object.defineProperty;var A=(c,s,a)=>s in c?v(c,s,{enumerable:!0,configurable:!0,writable:!0,value:a}):c[s]=a;var d=(c,s,a)=>(A(c,typeof s!="symbol"?s+"":s,a),a);var u;{let c={0:t=>m(t),1:t=>a(t),2:t=>new RegExp(t),3:t=>new Date(t),4:t=>new Map(a(t)),5:t=>new Set(a(t)),6:t=>BigInt(t),7:t=>new URL(t),8:t=>new Uint8Array(t),9:t=>new Uint16Array(t),10:t=>new Uint32Array(t)},s=t=>{let[e,n]=t;return e in c?c[e](n):void 0},a=t=>t.map(s),m=t=>typeof t!="object"||t===null?t:Object.fromEntries(Object.entries(t).map(([e,n])=>[e,s(n)]));customElements.get("astro-island")||customElements.define("astro-island",(u=class extends HTMLElement{constructor(){super(...arguments);d(this,"Component");d(this,"hydrator");d(this,"hydrate",async()=>{var f;if(!this.hydrator||!this.isConnected)return;let e=(f=this.parentElement)==null?void 0:f.closest("astro-island[ssr]");if(e){e.addEventListener("astro:hydrate",this.hydrate,{once:!0});return}let n=this.querySelectorAll("astro-slot"),r={},l=this.querySelectorAll("template[data-astro-template]");for(let o of l){let i=o.closest(this.tagName);i!=null&&i.isSameNode(this)&&(r[o.getAttribute("data-astro-template")||"default"]=o.innerHTML,o.remove())}for(let o of n){let i=o.closest(this.tagName);i!=null&&i.isSameNode(this)&&(r[o.getAttribute("name")||"default"]=o.innerHTML)}let h;try{h=this.hasAttribute("props")?m(JSON.parse(this.getAttribute("props"))):{}}catch(o){let i=this.getAttribute("component-url")||"<unknown>",b=this.getAttribute("component-export");throw b&&(i+=` (export ${b})`),console.error(`[hydrate] Error parsing props for component ${i}`,this.getAttribute("props"),o),o}let p;await this.hydrator(this)(this.Component,h,r,{client:this.getAttribute("client")}),this.removeAttribute("ssr"),this.dispatchEvent(new CustomEvent("astro:hydrate"))});d(this,"unmount",()=>{this.isConnected||this.dispatchEvent(new CustomEvent("astro:unmount"))})}disconnectedCallback(){document.removeEventListener("astro:after-swap",this.unmount),document.addEventListener("astro:after-swap",this.unmount,{once:!0})}connectedCallback(){if(!this.hasAttribute("await-children")||document.readyState==="interactive"||document.readyState==="complete")this.childrenConnectedCallback();else{let e=()=>{document.removeEventListener("DOMContentLoaded",e),n.disconnect(),this.childrenConnectedCallback()},n=new MutationObserver(()=>{var r;((r=this.lastChild)==null?void 0:r.nodeType)===Node.COMMENT_NODE&&this.lastChild.nodeValue==="astro:end"&&(this.lastChild.remove(),e())});n.observe(this,{childList:!0}),document.addEventListener("DOMContentLoaded",e)}}async childrenConnectedCallback(){let e=this.getAttribute("before-hydration-url");e&&await import(e),this.start()}async start(){let e=JSON.parse(this.getAttribute("opts")),n=this.getAttribute("client");if(Astro[n]===void 0){window.addEventListener(`astro:${n}`,()=>this.start(),{once:!0});return}try{await Astro[n](async()=>{let r=this.getAttribute("renderer-url"),[l,{default:h}]=await Promise.all([import(this.getAttribute("component-url")),r?import(r):()=>()=>{}]),p=this.getAttribute("component-export")||"default";if(!p.includes("."))this.Component=l[p];else{this.Component=l;for(let y of p.split("."))this.Component=this.Component[y]}return this.hydrator=h,this.hydrate},e,this)}catch(r){console.error(`[astro-island] Error hydrating ${this.getAttribute("component-url")}`,r)}}attributeChangedCallback(){this.hydrate()}},d(u,"observedAttributes",["props"]),u))}})();</script><astro-island uid="1iICUQ" component-url="/_astro/create-payment-button.CE6C2yQc.js" component-export="CreatePaymentButton" renderer-url="/_astro/client.DbokQZWz.js" props="{}" ssr="" client="only" opts="{&quot;name&quot;:&quot;CreatePaymentButton&quot;,&quot;value&quot;:true}"></astro-island> </main> </body> </html>
120 changes: 115 additions & 5 deletions apps/dapp/src/components/create-payment-button.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,131 @@
import { Button } from "@/components/ui/button";
import { payWithSPC } from "@/lib/utils";
import { WALLET_IFRAME_DIALOG_ID } from "@/lib/constants";
import { getPaymentOrigin, payWithSPC } from "@/lib/utils";
import { getAllowedCredentialsSchema } from "helpers";

const getIframe = () => {
const iframeDialog = document.getElementById(
WALLET_IFRAME_DIALOG_ID,
) as HTMLIFrameElement | null;

const iframe = iframeDialog?.querySelector("iframe");

return iframe;
};

const fallbackToIframeCredentialCreation = async () => {
// - trigger the iframe container to open so the user can create a credential
const iframeDialog = document.getElementById(
WALLET_IFRAME_DIALOG_ID,
) as HTMLDialogElement | null;

iframeDialog?.classList.remove("hidden");
iframeDialog?.classList.add("flex");

iframeDialog?.showModal();
};

/**
* Get the available credentials from the wallet iframe
*/
const getAvailableCredentials = async () => {
const iframe = getIframe();

if (!iframe) throw new Error("No iframe found");

const iframeOrigin = iframe?.src ? new URL(iframe.src).origin : undefined;

// - Create a Promise that resolves when the expected message is received
const createCredentialsPromise = (): Promise<string[]> => {
let timeout: NodeJS.Timeout | undefined;

const credentialsPromise: Promise<string[]> = new Promise(
(resolve, reject) => {
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
function handleMessage(event: any) {
const eventOrigin = new URL(event.origin).origin;
if (eventOrigin !== iframeOrigin) return;

console.log("received message", event);

switch (event.data.type) {
case "credentials.get": {
const parsedMessage = getAllowedCredentialsSchema.parse(
event.data,
);

resolve(parsedMessage.credentials);
}
}
// Remove the event listener to clean up
window.removeEventListener("message", handleMessage);
}

// Add an event listener to listen for messages from the iframe
window.addEventListener("message", handleMessage, false);

// Set a timeout to reject the promise if no response is received within a specific timeframe
timeout = setTimeout(() => {
window.removeEventListener("message", handleMessage);
reject(new Error("Timeout waiting for credentials response"));
}, 10000); // 10 seconds timeout
},
);

// ! Ensure to clean up on promise resolution or rejection
credentialsPromise.finally(() => clearTimeout(timeout));

return credentialsPromise;
};

// - Ask the iframe for the available credentials
iframe.contentWindow?.postMessage(
{ type: "credentials.get" },
getPaymentOrigin(),
);

return await createCredentialsPromise();
};

export function CreatePaymentButton() {
return (
<Button
onClick={async (e) => {
e.preventDefault();
console.log("Creating payment");

const allowedCredentials = await getAvailableCredentials();

if (!allowedCredentials || allowedCredentials.length === 0) {
return fallbackToIframeCredentialCreation();
}

await payWithSPC(
{
allowedCredentials: ["l8S4J0LWhvVdabcKL9tJcOApr5Qp44bi3SH88YCTOjQ"],
allowedCredentials,
challenge: "challenge",
timeout: 60000,
},
{
currency: "USDC",
value: "100.00",
// - the SPC spec allows for multiple items to be displayed
// - but currently the Chrome implementation does NOT display ANY items
displayItems: [
{
label: "NFT",
amount: { currency: "USD", value: "0.0000001" },
pending: true,
},
{
label: "Gas Fee",
amount: { currency: "USD", value: "0.0000001" },
pending: true,
},
],
total: {
label: "Total",
amount:
// - currency must be a ISO 4217 currency code
{ currency: "USD", value: "0.0000001" },
},
},
"0x",
);
Expand Down
42 changes: 42 additions & 0 deletions apps/dapp/src/components/wallet-iframe-dialog.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
---
import { cn } from "@/lib/utils";
import { WALLET_IFRAME_DIALOG_ID } from "@/lib/constants";
import { getPaymentOrigin } from "@/lib/utils";
const paymentOrigin = getPaymentOrigin();
---

<dialog
id={WALLET_IFRAME_DIALOG_ID}
class={cn(
"items-center justify-center",
"border rounded-lg py-3 px-4 min-h-[40vh]",
"backdrop:bg-black backdrop:opacity-50 backdrop:backdrop-blur-md",
"backdrop:transition-opacity backdrop:duration-300",
)}
>
<iframe
class="min-h-[40vh] border-0 m-0"
name="iframe"
allow={`payment ${paymentOrigin}`}></iframe>
</dialog>
<script>
import { WALLET_IFRAME_DIALOG_ID } from "@/lib/constants";
import { getPaymentOrigin } from "@/lib/utils";
const iframeDialog = document.getElementById(
WALLET_IFRAME_DIALOG_ID,
) as HTMLDialogElement | null;

// - remove the hidden class to show the iframe if present
iframeDialog?.classList.remove("hidden");

const iframe = iframeDialog?.querySelector("iframe");

if (iframe) iframe.src = getPaymentOrigin();

iframeDialog?.addEventListener("click", () => {
iframeDialog.close();
iframeDialog.removeAttribute("open");
iframeDialog.classList.add("hidden");
});
</script>
1 change: 1 addition & 0 deletions apps/dapp/src/env.d.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
/// <reference path="../.astro/types.d.ts" />
/// <reference types="astro/client" />
PUBLIC_PIMLICO_API_KEY
1 change: 1 addition & 0 deletions apps/dapp/src/lib/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const WALLET_IFRAME_DIALOG_ID = "wallet-iframe-dialog";
34 changes: 12 additions & 22 deletions apps/dapp/src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
import {
defaultInstrument,
fromBuffer,
getDappOrigin,
getWalletOrigin,
toBase64UrlString,
toBuffer,
} from "helpers";

Expand Down Expand Up @@ -33,7 +33,7 @@ type SerialisableCredentialRequestOptions = Omit<

export const payWithSPC = async (
requestOptions: SerialisableCredentialRequestOptions,
amount: PaymentCurrencyAmount,
paymentDetails: PaymentDetailsInit,
address: Address,
) => {
const { challenge, timeout } = requestOptions;
Expand Down Expand Up @@ -71,9 +71,8 @@ export const payWithSPC = async (
},
},
];
const paymentDetails = { total: { label: "Total", amount } };

console.log("spc paymentMethodData", paymentMethodData);
console.log("spc paymentMethodData", paymentMethodData, paymentDetails);

const request = new PaymentRequest(paymentMethodData, paymentDetails);

Expand All @@ -89,29 +88,20 @@ export const payWithSPC = async (
// response.details is a PublicKeyCredential, with a clientDataJSON that
// contains the transaction data for verification by the issuing bank.
const cred = response.details;
const credential = {

// TODO: send the wallet the response for verification before executing the payment
const serialisableCredential = {
id: cred.id,
type: cred.type,
// credential.rawId = base64url.encode(cred.rawId);
response: {
clientDataJSON: fromBuffer(cred.response.clientDataJSON),
authenticatorData: fromBuffer(cred.response.authenticatorData),
signature: fromBuffer(cred.response.signature),
userHandle: fromBuffer(cred.response.userHandle),
},
};

if (cred.response) {
const clientDataJSON = toBase64UrlString(cred.response.clientDataJSON);
const authenticatorData = toBase64UrlString(
cred.response.authenticatorData,
);
const signature = toBase64UrlString(cred.response.signature);
const userHandle = toBase64UrlString(cred.response.userHandle);
// @ts-ignore
credential.response = {
clientDataJSON,
authenticatorData,
signature,
userHandle,
};
}

// TODO: verify the response with the wallet
await response.complete("success");

/* send response.details to the issuing bank for verification */
Expand Down
Loading

0 comments on commit 4f1e5a9

Please sign in to comment.