Skip to content

Commit 51a1583

Browse files
authored
Feature/spec 2.1 input types (#15)
* feat: adjust the actions spec types * feat: refactor action components to use all new parameter types * feat: adjust action container, prepare for input layouts * feat: wip adjustments to layout * feat: implement styling based on input validation * feat: implement date, email, number, select, textarea, url inputs, improvements and adjustments * feat: implement checkbox group, adjustments to other components * feat: implement radio group, refactor ActionComponent * feat: add input factory to layout render * feat: fix focus-within, update icons for email and number, minor improvements * fix: number input validation, on click targeting the wrong component * feat: disable x default click behaviour, adjust email icon * feat: validate form as a whole, fix checkbox group callbacks * feat: adjust param description property name * chore: replace local types with @solana/actions-spec * fix: missing imports and failing types after upgrade * fix: initial valid state, initial form state * fix: form default values, query and body building * chore: remove pattern from textarea, since not supported yet * chore: remove magic string * fix: initially disabled radio/checkbox if has selected, adjustments to action components
1 parent 6d38544 commit 51a1583

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+2151
-478
lines changed

bun.lockb

380 Bytes
Binary file not shown.

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
"url": "https://github.com/dialectlabs/blinks"
1111
},
1212
"scripts": {
13-
"build": "tsup-node"
13+
"build": "tsup-node",
14+
"dev": "tsup-node --watch"
1415
},
1516
"main": "dist/index.cjs",
1617
"module": "dist/index.js",
@@ -42,6 +43,7 @@
4243
"dist"
4344
],
4445
"devDependencies": {
46+
"@solana/actions-spec": "^2.2.0",
4547
"@types/react": "^18.3.3",
4648
"@types/react-dom": "^18.3.0",
4749
"@typescript-eslint/eslint-plugin": "^7.16.1",

src/api/Action.ts

Lines changed: 0 additions & 209 deletions
This file was deleted.

src/api/Action/Action.ts

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
import { proxify, proxifyImage } from '../../utils/proxify.ts';
2+
import type { ActionAdapter } from '../ActionConfig.ts';
3+
import type {
4+
ActionGetResponse,
5+
ActionParameter,
6+
ActionParameterType,
7+
} from '../actions-spec.ts';
8+
import {
9+
type AbstractActionComponent,
10+
ButtonActionComponent,
11+
FormActionComponent,
12+
MultiValueActionComponent,
13+
SingleValueActionComponent,
14+
} from './action-components';
15+
16+
const MULTI_VALUE_TYPES: ActionParameterType[] = ['checkbox'];
17+
18+
interface ActionMetadata {
19+
blockchainIds: string[];
20+
}
21+
22+
export class Action {
23+
private readonly _actions: AbstractActionComponent[];
24+
25+
private constructor(
26+
private readonly _url: string,
27+
private readonly _data: ActionGetResponse,
28+
private readonly _metadata: ActionMetadata,
29+
private _adapter?: ActionAdapter,
30+
) {
31+
// if no links present, fallback to original solana pay spec
32+
if (!_data.links?.actions) {
33+
this._actions = [new ButtonActionComponent(this, _data.label, _url)];
34+
return;
35+
}
36+
37+
const urlObj = new URL(_url);
38+
this._actions = _data.links.actions.map((action) => {
39+
const href = action.href.startsWith('http')
40+
? action.href
41+
: urlObj.origin + action.href;
42+
43+
return componentFactory(this, action.label, href, action.parameters);
44+
});
45+
}
46+
47+
public get url() {
48+
return this._url;
49+
}
50+
51+
public get icon() {
52+
if (this._data.icon.startsWith('data:')) {
53+
return this._data.icon;
54+
}
55+
return proxifyImage(this._data.icon).toString();
56+
}
57+
58+
public get title() {
59+
return this._data.title;
60+
}
61+
62+
public get description() {
63+
return this._data.description;
64+
}
65+
66+
public get disabled() {
67+
return this._data.disabled ?? false;
68+
}
69+
70+
public get actions() {
71+
return this._actions;
72+
}
73+
74+
public get error() {
75+
return this._data.error?.message ?? null;
76+
}
77+
78+
public get metadata() {
79+
return this._metadata;
80+
}
81+
82+
public get adapter() {
83+
if (!this._adapter) {
84+
throw new Error('No adapter provided');
85+
}
86+
87+
return this._adapter;
88+
}
89+
90+
public setAdapter(adapter: ActionAdapter) {
91+
this._adapter = adapter;
92+
}
93+
94+
// be sure to use this only if the action is valid
95+
static hydrate(
96+
url: string,
97+
data: ActionGetResponse,
98+
metadata: ActionMetadata,
99+
adapter?: ActionAdapter,
100+
) {
101+
return new Action(url, data, metadata, adapter);
102+
}
103+
104+
static async fetch(apiUrl: string, adapter?: ActionAdapter) {
105+
const proxyUrl = proxify(apiUrl);
106+
const response = await fetch(proxyUrl, {
107+
headers: {
108+
Accept: 'application/json',
109+
},
110+
});
111+
112+
if (!response.ok) {
113+
throw new Error(
114+
`Failed to fetch action ${proxyUrl}, action url: ${apiUrl}`,
115+
);
116+
}
117+
118+
const data = (await response.json()) as ActionGetResponse;
119+
120+
// for multi-chain x-blockchain-ids
121+
const blockchainIds = (
122+
response?.headers?.get('x-blockchain-ids') || ''
123+
).split(',');
124+
125+
const metadata: ActionMetadata = {
126+
blockchainIds,
127+
};
128+
129+
return new Action(apiUrl, data, metadata, adapter);
130+
}
131+
}
132+
133+
const componentFactory = (
134+
parent: Action,
135+
label: string,
136+
href: string,
137+
parameters?: ActionParameter<ActionParameterType>[],
138+
): AbstractActionComponent => {
139+
if (!parameters?.length) {
140+
return new ButtonActionComponent(parent, label, href);
141+
}
142+
143+
if (parameters.length > 1) {
144+
return new FormActionComponent(parent, label, href, parameters);
145+
}
146+
147+
const [parameter] = parameters;
148+
149+
if (!parameter.type) {
150+
return new SingleValueActionComponent(parent, label, href, parameters);
151+
}
152+
153+
if (MULTI_VALUE_TYPES.includes(parameter.type)) {
154+
return new MultiValueActionComponent(parent, label, href, parameters);
155+
}
156+
157+
return new SingleValueActionComponent(parent, label, href, parameters);
158+
};

0 commit comments

Comments
 (0)