diff --git a/packages/blinks/package.json b/packages/blinks/package.json index 8ab7898d..65244da1 100644 --- a/packages/blinks/package.json +++ b/packages/blinks/package.json @@ -18,19 +18,24 @@ "types": "dist/index.d.ts", "exports": { "./ext/twitter": { + "types": "./dist/ext/twitter.d.ts", "import": "./dist/ext/twitter.js", - "require": "./dist/ext/twitter.cjs", - "types": "./dist/ext/twitter.d.ts" + "require": "./dist/ext/twitter.cjs" + }, + "./ext/youtube": { + "types": "./dist/ext/youtube.d.ts", + "import": "./dist/ext/youtube.js", + "require": "./dist/ext/youtube.cjs" }, "./hooks/solana": { + "types": "./dist/hooks/solana/index.d.ts", "import": "./dist/hooks/solana/index.js", - "require": "./dist/hooks/solana/index.cjs", - "types": "./dist/hooks/solana/index.d.ts" + "require": "./dist/hooks/solana/index.cjs" }, ".": { + "types": "./dist/index.d.ts", "import": "./dist/index.js", - "require": "./dist/index.cjs", - "types": "./dist/index.d.ts" + "require": "./dist/index.cjs" }, "./index.css": "./dist/index.css" }, @@ -61,7 +66,7 @@ "bs58": "^5.0.0", "@solana/wallet-adapter-react": "^0.15.0", "@solana/wallet-adapter-react-ui": "^0.9.0", - "@solana/web3.js": "^1.95.1", + "@solana/web3.js": "^1.95.4", "react": ">=18", "react-dom": ">=18" }, diff --git a/packages/blinks/src/ext/index.ts b/packages/blinks/src/ext/index.ts index 1d041f31..5f2da72f 100644 --- a/packages/blinks/src/ext/index.ts +++ b/packages/blinks/src/ext/index.ts @@ -1 +1,2 @@ export * from './twitter'; +export * from './youtube'; \ No newline at end of file diff --git a/packages/blinks/src/ext/youtube.tsx b/packages/blinks/src/ext/youtube.tsx new file mode 100644 index 00000000..859c9440 --- /dev/null +++ b/packages/blinks/src/ext/youtube.tsx @@ -0,0 +1,263 @@ +import { + Action, + type ActionAdapter, + type ActionCallbacksConfig, + type ActionsJsonConfig, + ActionsRegistry, + type ActionSupportStrategy, + ActionsURLMapper, + checkSecurity, + defaultActionSupportStrategy, + getExtendedActionState, + getExtendedInterstitialState, + getExtendedWebsiteState, + isInterstitial, + proxify, + type SecurityLevel, + } from '@dialectlabs/blinks-core'; + import { createRoot } from 'react-dom/client'; + import { Blink, type StylePreset } from '../ui'; + + type ObserverSecurityLevel = SecurityLevel; + + const noop = () => {}; + + export interface ObserverOptionsYT { + securityLevel: + | ObserverSecurityLevel + | Record<'websites' | 'interstitials' | 'actions', ObserverSecurityLevel>; + supportStrategy: ActionSupportStrategy; + } + + interface NormalizedObserverOptionsYT { + securityLevel: Record< + 'websites' | 'interstitials' | 'actions', + ObserverSecurityLevel + >; + supportStrategy: ActionSupportStrategy; + } + + const DEFAULT_OPTIONS: ObserverOptionsYT = { + securityLevel: 'only-trusted', + supportStrategy: defaultActionSupportStrategy, + }; + + const normalizeOptions = ( + options: Partial, + ): NormalizedObserverOptionsYT => { + return { + ...DEFAULT_OPTIONS, + ...options, + securityLevel: (() => { + if (!options.securityLevel) { + return { + websites: DEFAULT_OPTIONS.securityLevel as ObserverSecurityLevel, + interstitials: DEFAULT_OPTIONS.securityLevel as ObserverSecurityLevel, + actions: DEFAULT_OPTIONS.securityLevel as ObserverSecurityLevel, + }; + } + + if (typeof options.securityLevel === 'string') { + return { + websites: options.securityLevel, + interstitials: options.securityLevel, + actions: options.securityLevel, + }; + } + + return options.securityLevel; + })(), + }; + }; + + export function setupYouTubeObserver( + config: ActionAdapter, + callbacks: Partial = {}, + options: Partial = DEFAULT_OPTIONS, + ) { + const mergedOptions = normalizeOptions(options); + const youtubeCommentsSection = document.querySelector('ytd-comments#comments'); + + const refreshRegistry = async () => { + return ActionsRegistry.getInstance().init(); + }; + + refreshRegistry().then(() => { + const observer = new MutationObserver((mutations) => { + for (let i = 0; i < mutations.length; i++) { + const mutation = mutations[i]; + for (let j = 0; j < mutation.addedNodes.length; j++) { + const node = mutation.addedNodes[j]; + if (node.nodeType === Node.ELEMENT_NODE) { + handleNewYouTubeNode( + node as Element, + config, + callbacks, + mergedOptions, + ).catch(noop); + } + } + } + }); + + if (youtubeCommentsSection) { + observer.observe(youtubeCommentsSection, { childList: true, subtree: true }); + } + }); + } + + async function handleNewYouTubeNode( + node: Element, + config: ActionAdapter, + callbacks: Partial, + options: NormalizedObserverOptionsYT, + ) { + if (!node || node.nodeName !== 'YTD-COMMENT-THREAD-RENDERER') { + return; + } + + const contentElement = node.querySelector('#content-text'); + if (!contentElement) { + return; + } + + const anchorTags = Array.from(contentElement.querySelectorAll('a.yt-core-attributed-string__link')); + for (const anchor of anchorTags) { + const linkText = anchor.textContent?.trim(); + if (linkText && isValidUrl(linkText)) { + await processYouTubeLink(new URL(linkText), node, config, callbacks, options); + } + } + } + + async function processYouTubeLink( + originalUrl: URL, + commentNode: Element, + config: ActionAdapter, + callbacks: Partial, + options: NormalizedObserverOptionsYT, + ) { + const interstitialData = isInterstitial(originalUrl); + + let actionApiUrl: string | null; + + if (interstitialData.isInterstitial) { + const interstitialState = getExtendedInterstitialState(originalUrl.toString()); + if (!checkSecurity(interstitialState, options.securityLevel.interstitials)) { + return; + } + actionApiUrl = interstitialData.decodedActionUrl; + + } else { + const websiteState = getExtendedWebsiteState(originalUrl.toString()); + if (!checkSecurity(websiteState, options.securityLevel.websites)) { + return; + } + + const actionsJsonUrl = originalUrl.origin + '/actions.json'; + const actionsJson = await fetch(proxify(actionsJsonUrl)).then( + (res) => res.json() as Promise, + ); + + const actionsUrlMapper = new ActionsURLMapper(actionsJson); + actionApiUrl = actionsUrlMapper.mapUrl(originalUrl); + } + + const state = actionApiUrl ? getExtendedActionState(actionApiUrl) : null; + if ( + !actionApiUrl || + !state || + !checkSecurity(state, options.securityLevel.actions) + ) { + return; + } + + const action = await Action.fetch( + actionApiUrl, + config, + options.supportStrategy, + ).catch(noop); + + if (!action) { + return; + } + + const { container: actionContainer, reactRoot } = createYouTubeAction({ + originalUrl, + action, + callbacks, + options, + isInterstitial: interstitialData.isInterstitial, + }); + + const containerWrapper = document.createElement('div'); + containerWrapper.className = 'dialect-wrapper'; + containerWrapper.style.marginTop = '12px'; + containerWrapper.style.marginBottom = '12px'; + containerWrapper.style.maxWidth = '400px'; + containerWrapper.appendChild(actionContainer); + + commentNode.appendChild(containerWrapper); + + new MutationObserver((mutations, observer) => { + for (const mutation of mutations) { + for (const removedNode of Array.from(mutation.removedNodes)) { + if ( + removedNode === containerWrapper || + !document.body.contains(containerWrapper) + ) { + reactRoot.unmount(); + observer.disconnect(); + } + } + } + }).observe(document.body, { childList: true, subtree: true }); + } + + function createYouTubeAction({ + originalUrl, + action, + callbacks, + options, + }: { + originalUrl: URL; + action: Action; + callbacks: Partial; + options: NormalizedObserverOptionsYT; + isInterstitial: boolean; + }) { + + const container = document.createElement('div'); + container.className = 'dialect-action-root-container'; + + const actionRoot = createRoot(container); + + actionRoot.render( +
e.stopPropagation()}> + +
, + ); + + return { container, reactRoot: actionRoot }; + } + + const resolveYouTubeStylePreset = (): StylePreset => { + const darkTheme = document.documentElement.getAttribute('dark') === 'true'; + return darkTheme ? 'youtube-dark' : 'youtube-light'; + }; + + function isValidUrl(url: string): boolean { + try { + new URL(url); + return true; + } catch { + return false; + } + } \ No newline at end of file diff --git a/packages/blinks/src/hooks/solana/useActionSolanaWalletAdapter.ts b/packages/blinks/src/hooks/solana/useActionSolanaWalletAdapter.ts index e9860ea8..bbf72d93 100644 --- a/packages/blinks/src/hooks/solana/useActionSolanaWalletAdapter.ts +++ b/packages/blinks/src/hooks/solana/useActionSolanaWalletAdapter.ts @@ -63,8 +63,9 @@ export function useActionSolanaWalletAdapter( }, signTransaction: async (txData: string) => { try { + // Fixes the Buffer error on the base64 conversion const tx = await wallet.sendTransaction( - VersionedTransaction.deserialize(Buffer.from(txData, 'base64')), + VersionedTransaction.deserialize(new Uint8Array(Buffer.from(txData, 'base64'))), finalConnection, ); return { signature: tx }; diff --git a/packages/blinks/src/index.css b/packages/blinks/src/index.css index e81e9db2..f20d292f 100644 --- a/packages/blinks/src/index.css +++ b/packages/blinks/src/index.css @@ -72,7 +72,7 @@ --blink-shadow-container: 0px 129.333px 103.467px 0px rgba(0, 0, 0, 0.07), 0px 54.032px 43.226px 0px rgba(0, 0, 0, 0.05), 0px 16.195px 12.956px 0px rgba(0, 0, 0, 0.04), 0px 8.601px 6.881px 0px rgba(0, 0, 0, 0.03), 0px 3.579px 2.863px 0px rgba(0, 0, 0, 0.02); } - .x-dark, .x-light { + .x-dark, .x-light, .youtube-dark, .youtube-light { --blink-border-radius-rounded-lg: 0.25rem; --blink-border-radius-rounded-xl: 0.5rem; --blink-border-radius-rounded-2xl: 1.125rem; @@ -177,5 +177,114 @@ --blink-shadow-container: 0px 2px 8px 0px rgba(62, 177, 255, 0.22), 0px 1px 48px 0px rgba(62, 177, 255, 0.24); } - .custom {} + /* Css for Youtube - Dark */ + .youtube-dark { + --blink-bg-primary: #202327; + --blink-bg-secondary: #262a2d; + --blink-button: #f01d5d; + --blink-button-disabled: #2f3336; + --blink-button-hover: #f56993; + --blink-button-success: #00ae661a; + --blink-icon-error: #ff6565; + --blink-icon-error-hover: #ff7a7a; + --blink-icon-primary: #6e767d; + --blink-icon-primary-hover: #949ca4; + --blink-icon-warning: #ffb545; + --blink-icon-warning-hover: #ffc875; + --blink-input-bg: #202327; + --blink-input-bg-selected: #1d9bf0; + --blink-input-bg-disabled: #2f3336; + --blink-input-stroke: #3d4144; + --blink-input-stroke-disabled: #2f3336; + --blink-input-stroke-error: #ff6565; + --blink-input-stroke-hover: #6e767d; + --blink-input-stroke-selected: #1d9bf0; + --blink-stroke-error: #ff6565; + --blink-stroke-primary: #f01d5d; + --blink-stroke-secondary: #3d4144; + --blink-stroke-warning: #ffb545; + --blink-text-brand: #35aeff; + --blink-text-button: #fff; + --blink-text-button-disabled: #768088; + --blink-text-button-success: #12dc88; + --blink-text-error: #ff6565; + --blink-text-error-hover: #ff7a7a; + --blink-text-input: #fff; + --blink-text-input-disabled: #566470; + --blink-text-input-placeholder: #6e767d; + --blink-text-link: #6e767d; + --blink-text-link-hover: #949ca4; + --blink-text-primary: #fff; + --blink-text-secondary: #949ca4; + --blink-text-success: #12dc88; + --blink-text-warning: #ffb545; + --blink-text-warning-hover: #ffc875; + --blink-transparent-error: #aa00001a; + --blink-transparent-grey: #6e767d1a; + --blink-transparent-warning: #a966001a; + --blink-border-radius-rounded-lg: 0.25rem; + --blink-border-radius-rounded-xl: 0.5rem; + --blink-border-radius-rounded-2xl: 1.125rem; + --blink-border-radius-rounded-button: 600rem; + --blink-border-radius-rounded-input: 1.25rem; + --blink-border-radius-rounded-input-standalone: 1.75rem; + --blink-shadow-container: 0px 2px 8px 0px rgba(59,176,255,.22),0px 1px 48px 0px rgba(29,155,240,.24); + } + + /* Css for Youtube - Light */ + .youtube-light { + --blink-bg-primary: #ffffff; + --blink-bg-secondary: #262a2d; + --blink-button: #f01d5d; + --blink-button-disabled: #f01d5c40; + --blink-button-hover: #f05a87; + --blink-button-success: #00ae661a; + --blink-icon-error: #ff6565; + --blink-icon-error-hover: #ff7a7a; + --blink-icon-primary: #6e767d; + --blink-icon-primary-hover: #949ca4; + --blink-icon-warning: #ffb545; + --blink-icon-warning-hover: #ffc875; + --blink-input-bg: #ffffff; + --blink-input-bg-selected: #1d9bf0; + --blink-input-bg-disabled: #2f3336; + --blink-input-stroke: #3d4144; + --blink-input-stroke-disabled: #2f3336; + --blink-input-stroke-error: #ff6565; + --blink-input-stroke-hover: #6e767d; + --blink-input-stroke-selected: #1d9bf0; + --blink-stroke-error: #ff6565; + --blink-stroke-primary: #f01d5d; + --blink-stroke-secondary: #3d4144; + --blink-stroke-warning: #ffb545; + --blink-text-brand: #35aeff; + --blink-text-button: #fff; + --blink-text-button-disabled: #768088; + --blink-text-button-success: #12dc88; + --blink-text-error: #ff6565; + --blink-text-error-hover: #ff7a7a; + --blink-text-input: #fff; + --blink-text-input-disabled: #566470; + --blink-text-input-placeholder: #6e767d; + --blink-text-link: #6e767d; + --blink-text-link-hover: #949ca4; + --blink-text-primary: #fff; + --blink-text-secondary: #949ca4; + --blink-text-success: #12dc88; + --blink-text-warning: #ffb545; + --blink-text-warning-hover: #ffc875; + --blink-transparent-error: #aa00001a; + --blink-transparent-grey: #6e767d1a; + --blink-transparent-warning: #a966001a; + --blink-border-radius-rounded-lg: 0.25rem; + --blink-border-radius-rounded-xl: 0.5rem; + --blink-border-radius-rounded-2xl: 1.125rem; + --blink-border-radius-rounded-button: 600rem; + --blink-border-radius-rounded-input: 1.25rem; + --blink-border-radius-rounded-input-standalone: 1.75rem; + --blink-shadow-container: 0px 2px 8px 0px rgba(59,176,255,.22),0px 1px 48px 0px rgba(29,155,240,.24); +} + +.custom {} + } \ No newline at end of file diff --git a/packages/blinks/src/index.ts b/packages/blinks/src/index.ts index f85dc0b9..e0e59563 100644 --- a/packages/blinks/src/index.ts +++ b/packages/blinks/src/index.ts @@ -1,3 +1,5 @@ export * from '@dialectlabs/blinks-core'; export * from './ui'; + +export * from './ext'; \ No newline at end of file diff --git a/packages/blinks/src/ui/layouts/presets.ts b/packages/blinks/src/ui/layouts/presets.ts index b5295994..c1a0954c 100644 --- a/packages/blinks/src/ui/layouts/presets.ts +++ b/packages/blinks/src/ui/layouts/presets.ts @@ -4,5 +4,7 @@ export const themeClassMap: Record = { default: 'dial-light', 'x-dark': 'x-dark', 'x-light': 'x-light', + 'youtube-light': 'youtube-light', + 'youtube-dark': 'youtube-dark', custom: 'custom', }; diff --git a/packages/blinks/src/ui/types.ts b/packages/blinks/src/ui/types.ts index 66e255be..1261763f 100644 --- a/packages/blinks/src/ui/types.ts +++ b/packages/blinks/src/ui/types.ts @@ -1 +1 @@ -export type StylePreset = 'default' | 'x-dark' | 'x-light' | 'custom'; +export type StylePreset = 'default' | 'x-dark' | 'x-light' | 'youtube-dark' | 'youtube-light' | 'custom'; \ No newline at end of file diff --git a/postcss.config.js b/postcss.config.js index f39fe8a3..ebd7b58e 100644 --- a/postcss.config.js +++ b/postcss.config.js @@ -4,6 +4,8 @@ const selectorIgnoreList = [ '.x-dark', '.x-light', '.dial-light', + '.youtube-light', + '.youtube-dark', '.custom', ];