@nockchain/rose 0.1.4-nightly.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/artifacts.yml +33 -0
- package/.github/workflows/ci.yml +68 -0
- package/.github/workflows/publish-sdk.yml +35 -0
- package/.nvmrc +1 -0
- package/.prettierignore +5 -0
- package/.prettierrc +8 -0
- package/LICENSE +22 -0
- package/README.md +117 -0
- package/extension/background/index.ts +1500 -0
- package/extension/content/index.ts +59 -0
- package/extension/icons/rose.svg +27 -0
- package/extension/icons/rose128.png +0 -0
- package/extension/icons/rose16.png +0 -0
- package/extension/icons/rose256.png +0 -0
- package/extension/icons/rose32.png +0 -0
- package/extension/icons/rose48.png +0 -0
- package/extension/icons/rose512.png +0 -0
- package/extension/inpage/index.ts +86 -0
- package/extension/manifest.json +48 -0
- package/extension/popup/Popup.tsx +94 -0
- package/extension/popup/Router.tsx +121 -0
- package/extension/popup/assets/arrow-down-icon.svg +3 -0
- package/extension/popup/assets/arrow-left-icon.svg +3 -0
- package/extension/popup/assets/arrow-right-icon.svg +3 -0
- package/extension/popup/assets/arrow-up-icon.svg +3 -0
- package/extension/popup/assets/arrow-up-right-icon.svg +3 -0
- package/extension/popup/assets/checkmark-icon.svg +3 -0
- package/extension/popup/assets/checkmark-pencil-icon.svg +3 -0
- package/extension/popup/assets/checkmark-success-icon.svg +3 -0
- package/extension/popup/assets/clock-icon.svg +3 -0
- package/extension/popup/assets/close-x-icon.svg +3 -0
- package/extension/popup/assets/copy-icon.svg +6 -0
- package/extension/popup/assets/explorer-icon.svg +3 -0
- package/extension/popup/assets/eye-off-icon.svg +3 -0
- package/extension/popup/assets/eye-open-icon.svg +4 -0
- package/extension/popup/assets/feedback-icon.svg +3 -0
- package/extension/popup/assets/green-status-dot.svg +3 -0
- package/extension/popup/assets/info-icon.svg +3 -0
- package/extension/popup/assets/iris-logo-40.svg +27 -0
- package/extension/popup/assets/iris-logo-96.svg +27 -0
- package/extension/popup/assets/iris-logo-blue.svg +27 -0
- package/extension/popup/assets/iris-logo-no-eye.svg +27 -0
- package/extension/popup/assets/iris-logo-orange.svg +27 -0
- package/extension/popup/assets/iris-logo.svg +27 -0
- package/extension/popup/assets/key-icon.svg +3 -0
- package/extension/popup/assets/lock-icon-yellow.svg +3 -0
- package/extension/popup/assets/lock-icon.svg +3 -0
- package/extension/popup/assets/pencil-edit-icon.svg +3 -0
- package/extension/popup/assets/permissions-icon.svg +3 -0
- package/extension/popup/assets/receipt-icon.svg +5 -0
- package/extension/popup/assets/refresh-icon.svg +3 -0
- package/extension/popup/assets/settings-gear-icon.svg +8 -0
- package/extension/popup/assets/settings-icon.svg +3 -0
- package/extension/popup/assets/theme-icon.svg +3 -0
- package/extension/popup/assets/trash-bin-icon.svg +3 -0
- package/extension/popup/assets/trend-down-arrow.svg +5 -0
- package/extension/popup/assets/trend-up-arrow.svg +5 -0
- package/extension/popup/assets/user-account-icon.svg +3 -0
- package/extension/popup/assets/vector-bottom-left.svg +9 -0
- package/extension/popup/assets/vector-left.svg +9 -0
- package/extension/popup/assets/vector-right.svg +9 -0
- package/extension/popup/assets/vector-top-right-rotated.svg +8 -0
- package/extension/popup/assets/vector-top-right.svg +9 -0
- package/extension/popup/assets/wallet-dropdown-arrow.svg +5 -0
- package/extension/popup/assets/wallet-icon-style-1.svg +6 -0
- package/extension/popup/assets/wallet-icon-style-10.svg +8 -0
- package/extension/popup/assets/wallet-icon-style-11.svg +8 -0
- package/extension/popup/assets/wallet-icon-style-12.svg +8 -0
- package/extension/popup/assets/wallet-icon-style-13.svg +8 -0
- package/extension/popup/assets/wallet-icon-style-14.svg +8 -0
- package/extension/popup/assets/wallet-icon-style-15.svg +8 -0
- package/extension/popup/assets/wallet-icon-style-2.svg +8 -0
- package/extension/popup/assets/wallet-icon-style-3.svg +8 -0
- package/extension/popup/assets/wallet-icon-style-4.svg +8 -0
- package/extension/popup/assets/wallet-icon-style-5.svg +8 -0
- package/extension/popup/assets/wallet-icon-style-6.svg +8 -0
- package/extension/popup/assets/wallet-icon-style-7.svg +8 -0
- package/extension/popup/assets/wallet-icon-style-8.svg +8 -0
- package/extension/popup/assets/wallet-icon-style-9.svg +8 -0
- package/extension/popup/components/AccountIcon.tsx +78 -0
- package/extension/popup/components/AccountSelector.tsx +246 -0
- package/extension/popup/components/Alert.tsx +48 -0
- package/extension/popup/components/ConfirmModal.tsx +81 -0
- package/extension/popup/components/PasswordInput.tsx +49 -0
- package/extension/popup/components/ScreenContainer.tsx +17 -0
- package/extension/popup/components/SiteIcon.tsx +60 -0
- package/extension/popup/components/ThemeToggle.tsx +44 -0
- package/extension/popup/components/icons/ArrowDownLeftIcon.tsx +20 -0
- package/extension/popup/components/icons/ArrowUpRightIcon.tsx +20 -0
- package/extension/popup/components/icons/CheckIcon.tsx +20 -0
- package/extension/popup/components/icons/ChevronDownIcon.tsx +15 -0
- package/extension/popup/components/icons/ChevronLeftIcon.tsx +15 -0
- package/extension/popup/components/icons/ChevronRightIcon.tsx +15 -0
- package/extension/popup/components/icons/ChevronUpIcon.tsx +15 -0
- package/extension/popup/components/icons/CloseIcon.tsx +26 -0
- package/extension/popup/components/icons/CopyIcon.tsx +20 -0
- package/extension/popup/components/icons/EditIcon.tsx +20 -0
- package/extension/popup/components/icons/EyeIcon.tsx +13 -0
- package/extension/popup/components/icons/EyeOffIcon.tsx +13 -0
- package/extension/popup/components/icons/InfoIcon.tsx +20 -0
- package/extension/popup/components/icons/LockIcon.tsx +20 -0
- package/extension/popup/components/icons/PlusIcon.tsx +15 -0
- package/extension/popup/components/icons/ReceiveArrowIcon.tsx +14 -0
- package/extension/popup/components/icons/ReceiveCircleIcon.tsx +20 -0
- package/extension/popup/components/icons/SendPaperPlaneIcon.tsx +18 -0
- package/extension/popup/components/icons/SentArrowIcon.tsx +21 -0
- package/extension/popup/components/icons/SettingsIcon.tsx +26 -0
- package/extension/popup/components/icons/ShieldIcon.tsx +20 -0
- package/extension/popup/components/icons/UploadIcon.tsx +20 -0
- package/extension/popup/components/icons/WalletIcon.tsx +20 -0
- package/extension/popup/contexts/ThemeContext.tsx +105 -0
- package/extension/popup/hooks/useApprovalDetection.ts +128 -0
- package/extension/popup/hooks/useAutoFocus.ts +36 -0
- package/extension/popup/hooks/useAutoRejectOnClose.ts +25 -0
- package/extension/popup/hooks/useClickOutside.ts +33 -0
- package/extension/popup/hooks/useCopyToClipboard.ts +33 -0
- package/extension/popup/hooks/useFavicon.ts +64 -0
- package/extension/popup/hooks/useNumericInput.ts +93 -0
- package/extension/popup/index.html +13 -0
- package/extension/popup/index.tsx +24 -0
- package/extension/popup/screens/AboutScreen.tsx +118 -0
- package/extension/popup/screens/HomeScreen.tailwind.css +85 -0
- package/extension/popup/screens/HomeScreen.tsx +902 -0
- package/extension/popup/screens/KeySettingsPasswordScreen.tsx +164 -0
- package/extension/popup/screens/LockTimeScreen.tsx +155 -0
- package/extension/popup/screens/ReceiveScreen.tsx +149 -0
- package/extension/popup/screens/RecoveryPhraseScreen.tsx +183 -0
- package/extension/popup/screens/SendReviewScreen.tsx +308 -0
- package/extension/popup/screens/SendScreen.tsx +825 -0
- package/extension/popup/screens/SendSubmittedScreen.tsx +193 -0
- package/extension/popup/screens/SettingsScreen.tsx +116 -0
- package/extension/popup/screens/ThemeSettingsScreen.tsx +107 -0
- package/extension/popup/screens/TransactionDetailsScreen.tsx +346 -0
- package/extension/popup/screens/ViewSecretPhraseScreen.tsx +212 -0
- package/extension/popup/screens/WalletPermissionsScreen.tsx +123 -0
- package/extension/popup/screens/WalletSettingsScreen.tsx +381 -0
- package/extension/popup/screens/WalletStylingScreen.tsx +306 -0
- package/extension/popup/screens/approvals/ConnectApprovalScreen.tsx +136 -0
- package/extension/popup/screens/approvals/SignMessageScreen.tsx +140 -0
- package/extension/popup/screens/approvals/SignRawTxScreen.tsx +320 -0
- package/extension/popup/screens/approvals/TransactionApprovalScreen.tsx +167 -0
- package/extension/popup/screens/onboarding/BackupScreen.tsx +254 -0
- package/extension/popup/screens/onboarding/CreateScreen.tsx +273 -0
- package/extension/popup/screens/onboarding/ImportScreen.tsx +676 -0
- package/extension/popup/screens/onboarding/ImportScreenV0.tsx +678 -0
- package/extension/popup/screens/onboarding/ImportSuccessScreen.tsx +236 -0
- package/extension/popup/screens/onboarding/ResumeBackupScreen.tsx +166 -0
- package/extension/popup/screens/onboarding/StartScreen.tsx +142 -0
- package/extension/popup/screens/onboarding/SuccessScreen.tsx +193 -0
- package/extension/popup/screens/onboarding/VerifyScreen.tsx +220 -0
- package/extension/popup/screens/system/LockedScreen.tsx +288 -0
- package/extension/popup/screens/transactions/ReceiveScreen.tsx +84 -0
- package/extension/popup/screens/transactions/SentScreen.tsx +138 -0
- package/extension/popup/store.ts +482 -0
- package/extension/popup/styles.css +246 -0
- package/extension/popup/utils/format.ts +58 -0
- package/extension/popup/utils/formatWalletError.ts +36 -0
- package/extension/popup/utils/memo.ts +299 -0
- package/extension/popup/utils/messaging.ts +16 -0
- package/extension/shared/address-encoding.ts +69 -0
- package/extension/shared/balance-query.ts +123 -0
- package/extension/shared/constants.ts +386 -0
- package/extension/shared/currency.ts +128 -0
- package/extension/shared/first-name-derivation.ts +128 -0
- package/extension/shared/keyfile.ts +58 -0
- package/extension/shared/onboarding.ts +78 -0
- package/extension/shared/price-api.ts +79 -0
- package/extension/shared/rpc-client-browser.ts +315 -0
- package/extension/shared/transaction-builder.ts +443 -0
- package/extension/shared/types.ts +450 -0
- package/extension/shared/utxo-diff.ts +212 -0
- package/extension/shared/utxo-store.ts +548 -0
- package/extension/shared/utxo-sync.ts +343 -0
- package/extension/shared/validators.ts +26 -0
- package/extension/shared/vault.ts +1580 -0
- package/extension/shared/wallet-crypto.ts +77 -0
- package/extension/shared/wasm-utils.ts +76 -0
- package/extension/shared/webcrypto.ts +67 -0
- package/extension/types/wasm.d.ts +13 -0
- package/package.json +39 -0
- package/postcss.config.js +6 -0
- package/rose-extension-dist.zip +0 -0
- package/sdk/README.md +88 -0
- package/sdk/examples/app.ts +166 -0
- package/sdk/examples/index.html +51 -0
- package/sdk/examples/tsconfig.json +15 -0
- package/sdk/examples/tx-builder.html +532 -0
- package/sdk/examples/tx-builder.ts +1766 -0
- package/sdk/package-lock.json +424 -0
- package/sdk/package.json +68 -0
- package/sdk/src/constants.ts +28 -0
- package/sdk/src/errors.ts +74 -0
- package/sdk/src/hooks/index.ts +1 -0
- package/sdk/src/hooks/use-rose.ts +94 -0
- package/sdk/src/index.ts +12 -0
- package/sdk/src/provider.ts +396 -0
- package/sdk/src/transaction.ts +163 -0
- package/sdk/src/types/rose-wasm.d.ts +14 -0
- package/sdk/src/types.ts +97 -0
- package/sdk/src/wasm.ts +13 -0
- package/sdk/tsconfig.json +20 -0
- package/sdk/vite.config.examples.ts +32 -0
- package/tailwind.config.ts +38 -0
- package/tsconfig.json +20 -0
- package/vite.config.ts +60 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useApprovalDetection - Detect and handle approval requests from URL hash
|
|
3
|
+
*
|
|
4
|
+
* This hook monitors the URL hash for approval request parameters and automatically
|
|
5
|
+
* fetches the corresponding request data from the background script, then navigates
|
|
6
|
+
* to the appropriate approval screen when the wallet is unlocked.
|
|
7
|
+
*
|
|
8
|
+
* @param walletAddress - The current wallet address (null if not initialized)
|
|
9
|
+
* @param walletLocked - Whether the wallet is currently locked
|
|
10
|
+
* @param setPendingTransactionRequest - Function to set pending transaction request
|
|
11
|
+
* @param setPendingSignRequest - Function to set pending sign request
|
|
12
|
+
* @param navigate - Navigation function to switch screens
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { useEffect } from 'react';
|
|
16
|
+
import { send } from '../utils/messaging';
|
|
17
|
+
import { INTERNAL_METHODS, APPROVAL_CONSTANTS } from '../../shared/constants';
|
|
18
|
+
import type {
|
|
19
|
+
TransactionRequest,
|
|
20
|
+
SignRequest,
|
|
21
|
+
ConnectRequest,
|
|
22
|
+
SignRawTxRequest,
|
|
23
|
+
} from '../../shared/types';
|
|
24
|
+
import type { Screen } from '../store';
|
|
25
|
+
|
|
26
|
+
interface UseApprovalDetectionProps {
|
|
27
|
+
walletAddress: string | null;
|
|
28
|
+
walletLocked: boolean;
|
|
29
|
+
setPendingConnectRequest: (request: ConnectRequest | null) => void;
|
|
30
|
+
setPendingTransactionRequest: (request: TransactionRequest | null) => void;
|
|
31
|
+
setPendingSignRequest: (request: SignRequest | null) => void;
|
|
32
|
+
setPendingSignRawTxRequest: (request: SignRawTxRequest | null) => void;
|
|
33
|
+
navigate: (screen: Screen) => void;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function useApprovalDetection({
|
|
37
|
+
walletAddress,
|
|
38
|
+
walletLocked,
|
|
39
|
+
setPendingConnectRequest,
|
|
40
|
+
setPendingTransactionRequest,
|
|
41
|
+
setPendingSignRequest,
|
|
42
|
+
setPendingSignRawTxRequest,
|
|
43
|
+
navigate,
|
|
44
|
+
}: UseApprovalDetectionProps) {
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
// Wait for wallet state to be initialized
|
|
47
|
+
if (walletAddress === null) return;
|
|
48
|
+
|
|
49
|
+
const hash = window.location.hash.slice(1); // Remove '#'
|
|
50
|
+
|
|
51
|
+
if (hash.startsWith(APPROVAL_CONSTANTS.CONNECT_HASH_PREFIX)) {
|
|
52
|
+
const requestId = hash.replace(APPROVAL_CONSTANTS.CONNECT_HASH_PREFIX, '');
|
|
53
|
+
|
|
54
|
+
// Fetch pending connect request from background
|
|
55
|
+
send<ConnectRequest>(INTERNAL_METHODS.GET_PENDING_CONNECTION, [requestId])
|
|
56
|
+
.then(request => {
|
|
57
|
+
if (request && !('error' in request)) {
|
|
58
|
+
setPendingConnectRequest(request);
|
|
59
|
+
// Navigate to approval screen if unlocked, or locked screen if locked
|
|
60
|
+
if (!walletLocked) {
|
|
61
|
+
navigate('connect-approval');
|
|
62
|
+
} else {
|
|
63
|
+
navigate('locked');
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
})
|
|
67
|
+
.catch(console.error);
|
|
68
|
+
} else if (hash.startsWith(APPROVAL_CONSTANTS.TRANSACTION_HASH_PREFIX)) {
|
|
69
|
+
const requestId = hash.replace(APPROVAL_CONSTANTS.TRANSACTION_HASH_PREFIX, '');
|
|
70
|
+
|
|
71
|
+
// Fetch pending transaction request from background
|
|
72
|
+
send<TransactionRequest>(INTERNAL_METHODS.GET_PENDING_TRANSACTION, [requestId])
|
|
73
|
+
.then(request => {
|
|
74
|
+
if (request && !('error' in request)) {
|
|
75
|
+
setPendingTransactionRequest(request);
|
|
76
|
+
// Navigate to approval screen if unlocked, or locked screen if locked
|
|
77
|
+
if (!walletLocked) {
|
|
78
|
+
navigate('approve-transaction');
|
|
79
|
+
} else {
|
|
80
|
+
navigate('locked');
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
})
|
|
84
|
+
.catch(console.error);
|
|
85
|
+
} else if (hash.startsWith(APPROVAL_CONSTANTS.SIGN_MESSAGE_HASH_PREFIX)) {
|
|
86
|
+
const requestId = hash.replace(APPROVAL_CONSTANTS.SIGN_MESSAGE_HASH_PREFIX, '');
|
|
87
|
+
|
|
88
|
+
// Fetch pending sign request from background
|
|
89
|
+
send<SignRequest>(INTERNAL_METHODS.GET_PENDING_SIGN_REQUEST, [requestId])
|
|
90
|
+
.then(request => {
|
|
91
|
+
if (request && !('error' in request)) {
|
|
92
|
+
setPendingSignRequest(request);
|
|
93
|
+
// Navigate to approval screen if unlocked, or locked screen if locked
|
|
94
|
+
if (!walletLocked) {
|
|
95
|
+
navigate('sign-message');
|
|
96
|
+
} else {
|
|
97
|
+
navigate('locked');
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
})
|
|
101
|
+
.catch(console.error);
|
|
102
|
+
} else if (hash.startsWith(APPROVAL_CONSTANTS.SIGN_RAW_TX_HASH_PREFIX)) {
|
|
103
|
+
const requestId = hash.replace(APPROVAL_CONSTANTS.SIGN_RAW_TX_HASH_PREFIX, '');
|
|
104
|
+
|
|
105
|
+
// Fetch pending sign raw tx request from background
|
|
106
|
+
send<SignRawTxRequest>(INTERNAL_METHODS.GET_PENDING_SIGN_RAW_TX_REQUEST, [requestId])
|
|
107
|
+
.then(request => {
|
|
108
|
+
if (request && !('error' in request)) {
|
|
109
|
+
setPendingSignRawTxRequest(request);
|
|
110
|
+
// Navigate to approval screen if unlocked, or locked screen if locked
|
|
111
|
+
if (!walletLocked) {
|
|
112
|
+
navigate('approve-sign-raw-tx');
|
|
113
|
+
} else {
|
|
114
|
+
navigate('locked');
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
})
|
|
118
|
+
.catch(console.error);
|
|
119
|
+
}
|
|
120
|
+
}, [
|
|
121
|
+
walletAddress,
|
|
122
|
+
walletLocked,
|
|
123
|
+
setPendingConnectRequest,
|
|
124
|
+
setPendingTransactionRequest,
|
|
125
|
+
setPendingSignRequest,
|
|
126
|
+
navigate,
|
|
127
|
+
]);
|
|
128
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useAutoFocus - Auto-focus an input element
|
|
3
|
+
*
|
|
4
|
+
* @param options - Configuration options
|
|
5
|
+
* @param options.when - Condition that triggers focus (defaults to true for mount-only focus)
|
|
6
|
+
* @param options.select - Whether to select all text after focusing
|
|
7
|
+
* @returns Ref to attach to the input element
|
|
8
|
+
*
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { useRef, useEffect } from 'react';
|
|
12
|
+
|
|
13
|
+
interface UseAutoFocusOptions {
|
|
14
|
+
/** Condition that triggers focus. Defaults to true (focus on mount) */
|
|
15
|
+
when?: boolean | unknown;
|
|
16
|
+
/** Whether to select all text after focusing. Defaults to false */
|
|
17
|
+
select?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function useAutoFocus<T extends HTMLInputElement | HTMLTextAreaElement>(
|
|
21
|
+
options: UseAutoFocusOptions = {}
|
|
22
|
+
): React.RefObject<T | null> {
|
|
23
|
+
const { when = true, select = false } = options;
|
|
24
|
+
const ref = useRef<T>(null);
|
|
25
|
+
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
if (when && ref.current) {
|
|
28
|
+
ref.current.focus();
|
|
29
|
+
if (select) {
|
|
30
|
+
ref.current.select();
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}, [when, select]);
|
|
34
|
+
|
|
35
|
+
return ref;
|
|
36
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useAutoRejectOnClose - Auto-reject approval requests when window closes
|
|
3
|
+
*
|
|
4
|
+
* This hook automatically rejects pending approval requests when the user closes
|
|
5
|
+
* the popup window without taking action.
|
|
6
|
+
*
|
|
7
|
+
* @param requestId - The ID of the pending approval request
|
|
8
|
+
* @param rejectMethod - The internal method to call for rejection
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { useEffect } from 'react';
|
|
12
|
+
import { send } from '../utils/messaging';
|
|
13
|
+
|
|
14
|
+
export function useAutoRejectOnClose(requestId: string, rejectMethod: string) {
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
if (!requestId) return;
|
|
17
|
+
const handleBeforeUnload = () => {
|
|
18
|
+
// Reject the request when window is closing
|
|
19
|
+
send(rejectMethod, [requestId]).catch(console.error);
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
window.addEventListener('beforeunload', handleBeforeUnload);
|
|
23
|
+
return () => window.removeEventListener('beforeunload', handleBeforeUnload);
|
|
24
|
+
}, [requestId, rejectMethod]);
|
|
25
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useClickOutside - Detect clicks outside a referenced element
|
|
3
|
+
*
|
|
4
|
+
* @param ref - Reference to the element to detect clicks outside of
|
|
5
|
+
* @param handler - Callback function to execute when clicking outside
|
|
6
|
+
* @param enabled - Whether the listener is active (defaults to true)
|
|
7
|
+
*
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { useEffect, RefObject } from 'react';
|
|
11
|
+
|
|
12
|
+
export function useClickOutside<T extends HTMLElement>(
|
|
13
|
+
ref: RefObject<T | null>,
|
|
14
|
+
handler: (event: MouseEvent) => void,
|
|
15
|
+
enabled: boolean = true
|
|
16
|
+
): void {
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
if (!enabled) return;
|
|
19
|
+
|
|
20
|
+
function handleClickOutside(event: MouseEvent) {
|
|
21
|
+
// Check if click is outside the referenced element
|
|
22
|
+
if (ref.current && !ref.current.contains(event.target as Node)) {
|
|
23
|
+
handler(event);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Listen for mousedown events
|
|
28
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
29
|
+
|
|
30
|
+
// Cleanup listener on unmount or when dependencies change
|
|
31
|
+
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
32
|
+
}, [ref, handler, enabled]);
|
|
33
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useCopyToClipboard - Copy text to clipboard with state management
|
|
3
|
+
*
|
|
4
|
+
* @returns Object with copied state and copyToClipboard function
|
|
5
|
+
*
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { useState } from 'react';
|
|
9
|
+
|
|
10
|
+
interface UseCopyToClipboardReturn {
|
|
11
|
+
/** Whether text was recently copied (resets after 2 seconds) */
|
|
12
|
+
copied: boolean;
|
|
13
|
+
/** Copy text to clipboard */
|
|
14
|
+
copyToClipboard: (text: string) => Promise<void>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function useCopyToClipboard(): UseCopyToClipboardReturn {
|
|
18
|
+
const [copied, setCopied] = useState(false);
|
|
19
|
+
|
|
20
|
+
async function copyToClipboard(text: string) {
|
|
21
|
+
if (!text) return;
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
await navigator.clipboard.writeText(text);
|
|
25
|
+
setCopied(true);
|
|
26
|
+
setTimeout(() => setCopied(false), 2000);
|
|
27
|
+
} catch (err) {
|
|
28
|
+
console.error('Failed to copy to clipboard:', err);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return { copied, copyToClipboard };
|
|
33
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useFavicon - Fetch and cache website favicon
|
|
3
|
+
*
|
|
4
|
+
* Attempts to load favicon from origin's /favicon.ico
|
|
5
|
+
* Falls back to null if not found (triggers letter avatar fallback)
|
|
6
|
+
*
|
|
7
|
+
* Note: We intentionally don't use Google's favicon service because
|
|
8
|
+
* it returns a generic globe icon for unknown sites, which looks worse
|
|
9
|
+
* than a clean letter avatar.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { useState, useEffect } from 'react';
|
|
13
|
+
|
|
14
|
+
export function useFavicon(origin: string): string | null {
|
|
15
|
+
const [faviconUrl, setFaviconUrl] = useState<string | null>(null);
|
|
16
|
+
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
// Only fetch for http/https origins
|
|
19
|
+
if (!origin.startsWith('http://') && !origin.startsWith('https://')) {
|
|
20
|
+
setFaviconUrl(null);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let isCancelled = false;
|
|
25
|
+
|
|
26
|
+
async function loadFavicon() {
|
|
27
|
+
try {
|
|
28
|
+
const url = new URL(origin);
|
|
29
|
+
const domain = url.origin;
|
|
30
|
+
|
|
31
|
+
// Try direct favicon.ico
|
|
32
|
+
const directFavicon = `${domain}/favicon.ico`;
|
|
33
|
+
|
|
34
|
+
// Test if image loads successfully
|
|
35
|
+
const img = new Image();
|
|
36
|
+
img.onload = () => {
|
|
37
|
+
if (!isCancelled) {
|
|
38
|
+
setFaviconUrl(directFavicon);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
img.onerror = () => {
|
|
42
|
+
// No favicon found - fall back to null (letter avatar)
|
|
43
|
+
if (!isCancelled) {
|
|
44
|
+
setFaviconUrl(null);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
img.src = directFavicon;
|
|
48
|
+
} catch (err) {
|
|
49
|
+
// Invalid URL or other error - no favicon
|
|
50
|
+
if (!isCancelled) {
|
|
51
|
+
setFaviconUrl(null);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
loadFavicon();
|
|
57
|
+
|
|
58
|
+
return () => {
|
|
59
|
+
isCancelled = true;
|
|
60
|
+
};
|
|
61
|
+
}, [origin]);
|
|
62
|
+
|
|
63
|
+
return faviconUrl;
|
|
64
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom hook for managing numeric input fields with validation and increment/decrement
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { useState } from 'react';
|
|
6
|
+
|
|
7
|
+
interface UseNumericInputOptions {
|
|
8
|
+
/** Initial value (default: '') */
|
|
9
|
+
initialValue?: string;
|
|
10
|
+
/** Minimum allowed value (default: 0) */
|
|
11
|
+
min?: number;
|
|
12
|
+
/** Maximum allowed value (default: Infinity) */
|
|
13
|
+
max?: number;
|
|
14
|
+
/** Increment/decrement step size (default: 1) */
|
|
15
|
+
step?: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface UseNumericInputReturn {
|
|
19
|
+
/** Current input value as string */
|
|
20
|
+
value: string;
|
|
21
|
+
/** Set the value directly */
|
|
22
|
+
setValue: (value: string) => void;
|
|
23
|
+
/** Handle input change with validation (only allows numeric input) */
|
|
24
|
+
handleChange: (value: string) => void;
|
|
25
|
+
/** Increment by step amount */
|
|
26
|
+
increment: () => void;
|
|
27
|
+
/** Decrement by step amount */
|
|
28
|
+
decrement: () => void;
|
|
29
|
+
/** Get numeric value as number (returns 0 if empty) */
|
|
30
|
+
numericValue: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Hook for managing numeric input with validation and increment/decrement controls
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* const amount = useNumericInput({ min: 0, max: 100 });
|
|
38
|
+
*
|
|
39
|
+
* <input value={amount.value} onChange={(e) => amount.handleChange(e.target.value)} />
|
|
40
|
+
* <button onClick={amount.increment}>+</button>
|
|
41
|
+
* <button onClick={amount.decrement}>-</button>
|
|
42
|
+
*/
|
|
43
|
+
export function useNumericInput(options: UseNumericInputOptions = {}): UseNumericInputReturn {
|
|
44
|
+
const { initialValue = '', min = 0, max = Infinity, step = 1 } = options;
|
|
45
|
+
|
|
46
|
+
const [value, setValue] = useState(initialValue);
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Parse string value to number, defaulting to 0 if empty
|
|
50
|
+
*/
|
|
51
|
+
const getNumericValue = (val: string): number => {
|
|
52
|
+
return val === '' ? 0 : parseFloat(val);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Validate and set value if it passes numeric regex
|
|
57
|
+
*/
|
|
58
|
+
const handleChange = (newValue: string) => {
|
|
59
|
+
// Only allow numbers and decimal point
|
|
60
|
+
if (newValue === '' || /^\d*\.?\d*$/.test(newValue)) {
|
|
61
|
+
setValue(newValue);
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Increment by step, respecting max constraint
|
|
67
|
+
*/
|
|
68
|
+
const increment = () => {
|
|
69
|
+
const current = getNumericValue(value);
|
|
70
|
+
const newValue = Math.min(current + step, max);
|
|
71
|
+
setValue(newValue.toString());
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Decrement by step, respecting min constraint
|
|
76
|
+
*/
|
|
77
|
+
const decrement = () => {
|
|
78
|
+
const current = getNumericValue(value);
|
|
79
|
+
if (current > min) {
|
|
80
|
+
const newValue = Math.max(current - step, min);
|
|
81
|
+
setValue(newValue.toString());
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
value,
|
|
87
|
+
setValue,
|
|
88
|
+
handleChange,
|
|
89
|
+
increment,
|
|
90
|
+
decrement,
|
|
91
|
+
numericValue: getNumericValue(value),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>Rose</title>
|
|
7
|
+
<link rel="stylesheet" href="./styles.css" />
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<div id="root"></div>
|
|
11
|
+
<script type="module" src="./index.tsx"></script>
|
|
12
|
+
</body>
|
|
13
|
+
</html>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Popup entry point: Renders React app
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { createRoot } from 'react-dom/client';
|
|
6
|
+
import { Popup } from './Popup';
|
|
7
|
+
import { ThemeProvider } from './contexts/ThemeContext';
|
|
8
|
+
|
|
9
|
+
import './styles.css';
|
|
10
|
+
|
|
11
|
+
import '@fontsource/lora/400.css';
|
|
12
|
+
import '@fontsource/lora/500.css';
|
|
13
|
+
import '@fontsource/lora/600.css';
|
|
14
|
+
import '@fontsource/inter/400.css';
|
|
15
|
+
import '@fontsource/inter/500.css';
|
|
16
|
+
|
|
17
|
+
const root = document.getElementById('root');
|
|
18
|
+
if (root) {
|
|
19
|
+
createRoot(root).render(
|
|
20
|
+
<ThemeProvider>
|
|
21
|
+
<Popup />
|
|
22
|
+
</ThemeProvider>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { useStore } from '../store';
|
|
2
|
+
import RoseLogo96 from '../assets/iris-logo-96.svg';
|
|
3
|
+
import { ChevronLeftIcon } from '../components/icons/ChevronLeftIcon';
|
|
4
|
+
import { version } from '../../../package-lock.json';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* AboutScreen
|
|
8
|
+
*/
|
|
9
|
+
export function AboutScreen() {
|
|
10
|
+
const { navigate } = useStore();
|
|
11
|
+
|
|
12
|
+
function handleBack() {
|
|
13
|
+
navigate('settings');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function handleVisitWebsite() {
|
|
17
|
+
window.open('https://nocknames.com', '_blank');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function handleVisitNockchainWebsite() {
|
|
21
|
+
window.open('https://nockchain.net', '_blank');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function handleVisitNockboxWebsite() {
|
|
25
|
+
window.open('https://nockbox.org', '_blank');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<div
|
|
30
|
+
className="w-[357px] h-[600px] flex flex-col overflow-y-auto font-sans"
|
|
31
|
+
style={{ backgroundColor: 'var(--color-bg)', color: 'var(--color-text-primary)' }}
|
|
32
|
+
>
|
|
33
|
+
{/* Header */}
|
|
34
|
+
<header
|
|
35
|
+
className="flex items-center justify-between px-4 py-3 min-h-[64px]"
|
|
36
|
+
style={{ backgroundColor: 'var(--color-bg)' }}
|
|
37
|
+
>
|
|
38
|
+
<button
|
|
39
|
+
type="button"
|
|
40
|
+
onClick={handleBack}
|
|
41
|
+
aria-label="Back"
|
|
42
|
+
className="w-8 h-8 p-2 flex items-center justify-center rounded-lg flex-shrink-0 cursor-pointer transition-colors focus:outline-none focus-visible:ring-2"
|
|
43
|
+
style={{ color: 'var(--color-text-primary)' }}
|
|
44
|
+
onMouseEnter={e => (e.currentTarget.style.backgroundColor = 'var(--color-surface-800)')}
|
|
45
|
+
onMouseLeave={e => (e.currentTarget.style.backgroundColor = 'transparent')}
|
|
46
|
+
>
|
|
47
|
+
<ChevronLeftIcon className="w-5 h-5" />
|
|
48
|
+
</button>
|
|
49
|
+
|
|
50
|
+
<h1 className="m-0 text-base font-medium leading-[22px] tracking-[0.16px]">About</h1>
|
|
51
|
+
|
|
52
|
+
<div className="w-8 h-8 flex-shrink-0" />
|
|
53
|
+
</header>
|
|
54
|
+
|
|
55
|
+
{/* Content */}
|
|
56
|
+
<div className="flex flex-col items-center gap-6 px-4 py-2 h-[536px]">
|
|
57
|
+
<div className="w-24 h-24 flex items-center justify-center flex-shrink-0">
|
|
58
|
+
<img src={RoseLogo96} alt="Rose" className="w-24 h-24" />
|
|
59
|
+
</div>
|
|
60
|
+
|
|
61
|
+
<div className="w-full flex flex-col items-center justify-center gap-1">
|
|
62
|
+
<p className="m-0 text-base font-medium leading-[22px] tracking-[0.16px] text-center">
|
|
63
|
+
Rose Version {version}
|
|
64
|
+
</p>
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
<div className="w-full h-px" style={{ backgroundColor: 'var(--color-divider)' }} />
|
|
68
|
+
|
|
69
|
+
<div className="w-full flex flex-col gap-3">
|
|
70
|
+
<h2
|
|
71
|
+
className="m-0 text-sm font-medium leading-[18px] tracking-[0.14px]"
|
|
72
|
+
style={{ color: 'var(--color-text-muted)' }}
|
|
73
|
+
>
|
|
74
|
+
Links
|
|
75
|
+
</h2>
|
|
76
|
+
|
|
77
|
+
<div className="w-full flex flex-col gap-4">
|
|
78
|
+
<button
|
|
79
|
+
type="button"
|
|
80
|
+
onClick={handleVisitWebsite}
|
|
81
|
+
className="text-left bg-transparent p-0 text-base font-medium leading-[22px] tracking-[0.16px] underline underline-offset-2 transition-opacity hover:opacity-70 focus:outline-none focus-visible:ring-2"
|
|
82
|
+
>
|
|
83
|
+
nocknames.com
|
|
84
|
+
</button>
|
|
85
|
+
<button
|
|
86
|
+
type="button"
|
|
87
|
+
onClick={handleVisitNockchainWebsite}
|
|
88
|
+
className="text-left bg-transparent p-0 text-base font-medium leading-[22px] tracking-[0.16px] underline underline-offset-2 transition-opacity hover:opacity-70 focus:outline-none focus-visible:ring-2"
|
|
89
|
+
>
|
|
90
|
+
Copyright (c) 2026 nockchain.net LLC oss@nockchain.net
|
|
91
|
+
</button>
|
|
92
|
+
<button
|
|
93
|
+
type="button"
|
|
94
|
+
onClick={() =>
|
|
95
|
+
window.open(
|
|
96
|
+
'https://raw.githubusercontent.com/nockbox/iris/refs/heads/main/LICENSE',
|
|
97
|
+
'_blank'
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
className="text-left bg-transparent p-0 text-base font-medium leading-[22px] tracking-[0.16px] underline underline-offset-2 transition-opacity hover:opacity-70 focus:outline-none focus-visible:ring-2"
|
|
101
|
+
>
|
|
102
|
+
Copyright (c) 2025 NockBox inc. tech@nockbox.org
|
|
103
|
+
</button>
|
|
104
|
+
<button
|
|
105
|
+
type="button"
|
|
106
|
+
onClick={() =>
|
|
107
|
+
window.open('https://www.vecteezy.com/free-vector/simple-rose', '_blank')
|
|
108
|
+
}
|
|
109
|
+
className="text-left bg-transparent p-0 text-base font-medium leading-[22px] tracking-[0.16px] underline underline-offset-2 transition-opacity hover:opacity-70 focus:outline-none focus-visible:ring-2"
|
|
110
|
+
>
|
|
111
|
+
Simple Rose Vectors by Vecteezy
|
|
112
|
+
</button>
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
);
|
|
118
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
@tailwind base;
|
|
2
|
+
@tailwind components;
|
|
3
|
+
@tailwind utilities;
|
|
4
|
+
|
|
5
|
+
html,
|
|
6
|
+
body,
|
|
7
|
+
#root {
|
|
8
|
+
height: 100%;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
:root {
|
|
12
|
+
--header-h: 120px;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/* fallback if JS measuring fails */
|
|
16
|
+
|
|
17
|
+
/* Optional nicer scrollbar inside the popup */
|
|
18
|
+
.scroll-thin::-webkit-scrollbar {
|
|
19
|
+
width: 8px;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.scroll-thin::-webkit-scrollbar-thumb {
|
|
23
|
+
background: rgba(0, 0, 0, 0.15);
|
|
24
|
+
border-radius: 6px;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/* Wallet dropdown hover effect - show settings icon on hover */
|
|
28
|
+
.wallet-dropdown-item .wallet-balance {
|
|
29
|
+
display: block;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.wallet-dropdown-item .wallet-settings-icon {
|
|
33
|
+
display: none;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.wallet-dropdown-item:hover {
|
|
37
|
+
background-color: var(--color-surface-900);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.wallet-dropdown-item:hover .wallet-balance {
|
|
41
|
+
display: none;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.wallet-dropdown-item:hover .wallet-settings-icon {
|
|
45
|
+
display: flex;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/* Shimmer loading animation */
|
|
49
|
+
@keyframes shimmer {
|
|
50
|
+
0% {
|
|
51
|
+
background-position: -468px 0;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
100% {
|
|
55
|
+
background-position: 468px 0;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/* Dark mode skeleton (default) */
|
|
60
|
+
.skeleton-shimmer {
|
|
61
|
+
animation: shimmer 10s linear infinite;
|
|
62
|
+
background: linear-gradient(
|
|
63
|
+
to right,
|
|
64
|
+
rgba(255, 255, 255, 0.05) 0%,
|
|
65
|
+
rgba(255, 255, 255, 0.12) 20%,
|
|
66
|
+
rgba(255, 255, 255, 0.18) 40%,
|
|
67
|
+
rgba(255, 255, 255, 0.12) 60%,
|
|
68
|
+
rgba(255, 255, 255, 0.05) 80%,
|
|
69
|
+
rgba(255, 255, 255, 0.05) 100%
|
|
70
|
+
);
|
|
71
|
+
background-size: 800px 100px;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/* Light mode skeleton */
|
|
75
|
+
.light .skeleton-shimmer {
|
|
76
|
+
background: linear-gradient(
|
|
77
|
+
to right,
|
|
78
|
+
rgba(0, 0, 0, 0.08) 0%,
|
|
79
|
+
rgba(0, 0, 0, 0.15) 20%,
|
|
80
|
+
rgba(0, 0, 0, 0.2) 40%,
|
|
81
|
+
rgba(0, 0, 0, 0.15) 60%,
|
|
82
|
+
rgba(0, 0, 0, 0.08) 80%,
|
|
83
|
+
rgba(0, 0, 0, 0.08) 100%
|
|
84
|
+
);
|
|
85
|
+
}
|