Skip to content

Commit 346d318

Browse files
authored
Feature/hook callback improvements (#40)
* feat: adjust hooks with refresh, destroy registry refresh interval * feat: increase amount of callbacks * feat: adjust default value
1 parent 418e95a commit 346d318

File tree

10 files changed

+135
-33
lines changed

10 files changed

+135
-33
lines changed

packages/blinks-core/src/BlinkContainer.tsx

Lines changed: 50 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
SingleValueActionComponent,
2828
} from './api';
2929
import { checkSecurity, isInterstitial, type SecurityLevel } from './utils';
30+
import { EMPTY_OBJECT } from './utils/constants.ts';
3031
import {
3132
isPostRequestError,
3233
isSignMessageError,
@@ -272,7 +273,7 @@ export const BlinkContainer = ({
272273
adapter,
273274
websiteUrl,
274275
websiteText,
275-
callbacks,
276+
callbacks = EMPTY_OBJECT,
276277
securityLevel = DEFAULT_SECURITY_LEVEL,
277278
Layout,
278279
selector,
@@ -339,14 +340,14 @@ export const BlinkContainer = ({
339340
}, [initialAction, websiteUrl]);
340341

341342
useEffect(() => {
342-
callbacks?.onActionMount?.(
343-
action,
344-
websiteUrl ?? action.url,
343+
callbacks.onActionMount?.(
344+
initialAction,
345+
websiteUrl ?? initialAction.url,
345346
actionState.action,
346347
);
347-
// we run this effect ONLY if the action changes
348+
// we run this effect ONLY once
348349
// eslint-disable-next-line react-hooks/exhaustive-deps
349-
}, [action.id]);
350+
}, []);
350351

351352
useEffect(() => {
352353
const liveDataConfig = action.liveData_experimental;
@@ -454,6 +455,7 @@ export const BlinkContainer = ({
454455
) {
455456
setActionState(newActionState);
456457
dispatch({ type: ExecutionType.BLOCK });
458+
callbacks.onActionCancel?.(action, component, 'security-state-changed');
457459
return;
458460
}
459461

@@ -470,6 +472,7 @@ export const BlinkContainer = ({
470472
const account = await adapter.connect(context);
471473
if (!account) {
472474
dispatch({ type: ExecutionType.RESET });
475+
callbacks?.onActionCancel?.(action, component, 'wallet-not-connected');
473476
return;
474477
}
475478

@@ -484,6 +487,7 @@ export const BlinkContainer = ({
484487
? response.error
485488
: 'Transaction data missing',
486489
});
490+
callbacks.onActionError?.(action, component, 'post-request-error');
487491
return;
488492
}
489493

@@ -493,6 +497,7 @@ export const BlinkContainer = ({
493497
type: ExecutionType.FINISH,
494498
successMessage: response.message,
495499
});
500+
callbacks.onActionComplete?.(action, component, signature);
496501
return;
497502
}
498503

@@ -501,6 +506,11 @@ export const BlinkContainer = ({
501506
type: ExecutionType.SOFT_RESET,
502507
errorMessage: 'Missing signature for message',
503508
});
509+
callbacks.onActionError?.(
510+
action,
511+
component,
512+
'message-signature-missing',
513+
);
504514
return;
505515
}
506516

@@ -525,11 +535,19 @@ export const BlinkContainer = ({
525535
type: ExecutionType.FINISH,
526536
successMessage: response.message,
527537
});
538+
callbacks.onActionComplete?.(action, component, signature);
528539
return;
529540
}
530541

531542
setAction(nextAction);
532543
dispatch({ type: ExecutionType.RESET });
544+
callbacks.onActionChain?.(
545+
action,
546+
nextAction,
547+
component,
548+
response.type,
549+
signature,
550+
);
533551
};
534552

535553
if (response.type === 'transaction' || !response.type) {
@@ -540,10 +558,33 @@ export const BlinkContainer = ({
540558

541559
if (!signResult || isSignTransactionError(signResult)) {
542560
dispatch({ type: ExecutionType.RESET });
561+
callbacks.onActionCancel?.(
562+
action,
563+
component,
564+
'transaction-sign-cancel',
565+
);
543566
return;
544567
}
545568

546-
await adapter.confirmTransaction(signResult.signature, context);
569+
const confirmationResult = await adapter
570+
.confirmTransaction(signResult.signature, context)
571+
.then(() => ({ success: true as const }))
572+
.catch((e) => ({ success: false as const, message: e.message }));
573+
574+
if (!confirmationResult.success) {
575+
dispatch({
576+
type: ExecutionType.SOFT_RESET,
577+
errorMessage:
578+
confirmationResult.message ?? 'Unknown error, please try again',
579+
});
580+
callbacks.onActionError?.(
581+
action,
582+
component,
583+
'transaction-confirmation-failed',
584+
signResult.signature,
585+
);
586+
return;
587+
}
547588

548589
await chain(signResult.signature);
549590
return;
@@ -554,6 +595,7 @@ export const BlinkContainer = ({
554595

555596
if (!signResult || isSignMessageError(signResult)) {
556597
dispatch({ type: ExecutionType.RESET });
598+
callbacks.onActionCancel?.(action, component, 'message-sign-cancel');
557599
return;
558600
}
559601

@@ -585,6 +627,7 @@ export const BlinkContainer = ({
585627
type: ExecutionType.SOFT_RESET,
586628
errorMessage: (e as Error).message ?? 'Unknown error, please try again',
587629
});
630+
callbacks.onActionError?.(action, component, 'unknown-error');
588631
}
589632
};
590633

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,38 @@
1-
import { Action } from './Action';
1+
import type { LinkedActionType } from '@solana/actions-spec';
2+
import { AbstractActionComponent, Action } from './Action';
23

34
export interface ActionCallbacksConfig {
5+
// Initial action mount (called once)
46
onActionMount: (
57
action: Action,
68
originalUrl: string,
79
type: 'trusted' | 'malicious' | 'unknown',
810
) => void;
11+
// Action execution was cancelled (e.g. user interaction or blocked from execution)
12+
onActionCancel: (
13+
action: Action,
14+
trigger: AbstractActionComponent,
15+
reason: string,
16+
) => void;
17+
// Action executed and chained to the next action
18+
onActionChain: (
19+
previousAction: Action,
20+
chainedAction: Action,
21+
chainTrigger: AbstractActionComponent,
22+
chainType: LinkedActionType,
23+
signature?: string,
24+
) => void;
25+
// Action execution completed fully (called once at the end of the chain or single action)
26+
onActionComplete: (
27+
action: Action,
28+
trigger: AbstractActionComponent,
29+
signature?: string,
30+
) => void;
31+
// Action execution failed (e.g. network error, invalid response, timeout). Does not include action cancellation.
32+
onActionError: (
33+
action: Action,
34+
trigger: AbstractActionComponent,
35+
reason: string,
36+
signature?: string,
37+
) => void;
938
}

packages/blinks-core/src/api/ActionsRegistry.ts

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export class ActionsRegistry {
1010
private websitesByHost: Record<string, RegisteredEntity>;
1111
private interstitialsByHost: Record<string, RegisteredEntity>;
1212

13-
private initPromise: Promise<ActionsRegistryConfig> | null = null;
13+
private intervalId: NodeJS.Timeout | null = null;
1414

1515
private constructor(config?: ActionsRegistryConfig) {
1616
this.actionsByHost = config
@@ -43,16 +43,25 @@ export class ActionsRegistry {
4343
}
4444

4545
public async init(): Promise<void> {
46-
if (this.initPromise !== null) {
46+
if (this.intervalId !== null) {
4747
return;
4848
}
4949
await this.refresh();
50-
setInterval(() => this.refresh(), DEFAULT_REFRESH_INTERVAL);
50+
this.intervalId = setInterval(
51+
() => this.refresh(),
52+
DEFAULT_REFRESH_INTERVAL,
53+
);
54+
}
55+
56+
public stopRefresh(): void {
57+
if (this.intervalId !== null) {
58+
clearInterval(this.intervalId);
59+
this.intervalId = null;
60+
}
5161
}
5262

5363
public async refresh(): Promise<void> {
54-
this.initPromise = fetchActionsRegistryConfig();
55-
const config = await this.initPromise;
64+
const config = await fetchActionsRegistryConfig();
5665
this.actionsByHost = Object.fromEntries(
5766
config.actions.map((action) => [action.host, action]),
5867
);

packages/blinks-core/src/hooks/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
export { useAction } from './useAction';
22
export { useActionsRegistryInterval } from './useActionRegistryInterval';
33
export {
4+
fetchBlinkList,
45
useBlinkList,
56
type BlinkList,
67
type BlinkListEntry,
78
} from './useBlinkList.ts';
89
export {
10+
fetchMetadata,
911
useMetadata,
1012
type BlinkMetadata,
1113
type MetadataRow,

packages/blinks-core/src/hooks/useAction.ts

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useEffect, useState } from 'react';
1+
import { useCallback, useEffect, useState } from 'react';
22
import {
33
Action,
44
type ActionSupportStrategy,
@@ -52,25 +52,26 @@ export function useAction({
5252
const [isLoading, setIsLoading] = useState(false);
5353
const [hasFetched, setHasFetched] = useState(false);
5454

55-
useEffect(() => {
56-
setIsLoading(true);
57-
if (!isRegistryLoaded || !actionApiUrl) {
58-
return;
55+
const fetchAction = useCallback(() => {
56+
if (!actionApiUrl) {
57+
return () => {};
5958
}
6059

6160
let ignore = false;
61+
setIsLoading(true);
6262
setHasFetched(false);
6363
Action.fetch(actionApiUrl, supportStrategy)
6464
.then((action) => {
65-
if (ignore) {
66-
return;
65+
if (!ignore) {
66+
setAction(action);
67+
setHasFetched(true);
6768
}
68-
setAction(action);
69-
setHasFetched(true);
7069
})
7170
.catch((e) => {
72-
console.error('[@dialectlabs/blinks-core] Failed to fetch action', e);
73-
setAction(null);
71+
if (!ignore) {
72+
console.error('[@dialectlabs/blinks-core] Failed to fetch action', e);
73+
setAction(null);
74+
}
7475
})
7576
.finally(() => {
7677
if (!ignore) {
@@ -81,7 +82,19 @@ export function useAction({
8182
return () => {
8283
ignore = true;
8384
};
84-
// eslint-disable-next-line react-hooks/exhaustive-deps -- only update if actionApiUrl changes
85+
}, [actionApiUrl, supportStrategy]);
86+
87+
useEffect(() => {
88+
if (!isRegistryLoaded) {
89+
return;
90+
}
91+
92+
const cleanup = fetchAction();
93+
94+
return () => {
95+
cleanup();
96+
};
97+
// eslint-disable-next-line react-hooks/exhaustive-deps -- only update if actionApiUrl changes or registry loaded
8598
}, [actionApiUrl, isRegistryLoaded]);
8699

87100
// this effect handles race conditions between fetching the action support strategy changes
@@ -101,5 +114,5 @@ export function useAction({
101114
// eslint-disable-next-line react-hooks/exhaustive-deps -- only update if supportStrategy changes
102115
}, [supportStrategy, hasFetched]);
103116

104-
return { action, isLoading };
117+
return { action, isLoading, refresh: fetchAction };
105118
}

packages/blinks-core/src/hooks/useActionRegistryInterval.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,14 @@ export function useActionsRegistryInterval() {
55
const [isRegistryLoaded, setRegistryLoaded] = useState(false);
66

77
useEffect(() => {
8-
ActionsRegistry.getInstance()
9-
.init()
10-
.then(() => {
11-
setRegistryLoaded(true);
12-
});
8+
const registry = ActionsRegistry.getInstance();
9+
registry.init().then(() => {
10+
setRegistryLoaded(true);
11+
});
12+
13+
return () => {
14+
registry.stopRefresh();
15+
};
1316
}, [isRegistryLoaded]);
1417

1518
return { isRegistryLoaded };

packages/blinks-core/src/hooks/useBlinkList.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ export const useBlinkList = () => {
5555
};
5656
};
5757

58-
async function fetchBlinkList(): Promise<BlinkList> {
58+
export async function fetchBlinkList(): Promise<BlinkList> {
5959
try {
6060
const response = await fetch(
6161
'https://registry.dial.to/v1/private/blinks/list',

packages/blinks-core/src/hooks/useMetadata.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ export const useMetadata = ({ url, wallet }: UseMetadataArgs) => {
5959
};
6060
};
6161

62-
async function fetchMetadata(
62+
export async function fetchMetadata(
6363
url: string,
6464
wallet?: string,
6565
): Promise<BlinkMetadata> {
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
11
export const SOLANA_ACTION_PREFIX = /^(solana-action:|solana:)/;
2+
3+
export const EMPTY_OBJECT = Object.freeze({});

packages/blinks-core/src/utils/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export { BlockchainIds } from './caip-2.ts';
22
export { BLINK_CLIENT_KEY_HEADER, setClientKey } from './client-key.ts';
3+
export { SOLANA_ACTION_PREFIX } from './constants.ts';
34
export * from './interstitial-url.ts';
45
export { proxify, proxifyImage, setProxyUrl } from './proxify';
56
export { checkSecurity, type SecurityLevel } from './security';

0 commit comments

Comments
 (0)