diff --git a/browser-extension/background.js b/browser-extension/background.js
new file mode 100644
index 0000000..31120cb
--- /dev/null
+++ b/browser-extension/background.js
@@ -0,0 +1,247 @@
+const DEFAULT_HOST = 'https://rnotes.online';
+
+// --- Context Menu Setup ---
+
+chrome.runtime.onInstalled.addListener(() => {
+ chrome.contextMenus.create({
+ id: 'clip-page',
+ title: 'Clip page to rNotes',
+ contexts: ['page'],
+ });
+
+ chrome.contextMenus.create({
+ id: 'save-link',
+ title: 'Save link to rNotes',
+ contexts: ['link'],
+ });
+
+ chrome.contextMenus.create({
+ id: 'save-image',
+ title: 'Save image to rNotes',
+ contexts: ['image'],
+ });
+
+ chrome.contextMenus.create({
+ id: 'clip-selection',
+ title: 'Clip selection to rNotes',
+ contexts: ['selection'],
+ });
+});
+
+// --- Helpers ---
+
+async function getSettings() {
+ const result = await chrome.storage.sync.get(['rnotesHost']);
+ return {
+ host: result.rnotesHost || DEFAULT_HOST,
+ };
+}
+
+async function getToken() {
+ const result = await chrome.storage.local.get(['encryptid_token']);
+ return result.encryptid_token || null;
+}
+
+async function getDefaultNotebook() {
+ const result = await chrome.storage.local.get(['lastNotebookId']);
+ return result.lastNotebookId || null;
+}
+
+function showNotification(title, message) {
+ chrome.notifications.create({
+ type: 'basic',
+ iconUrl: 'icons/icon-128.png',
+ title: title,
+ message: message,
+ });
+}
+
+async function createNote(data) {
+ const token = await getToken();
+ if (!token) {
+ showNotification('rNotes Error', 'Not signed in. Open extension settings to sign in.');
+ return;
+ }
+
+ const settings = await getSettings();
+ const notebookId = await getDefaultNotebook();
+
+ const body = {
+ title: data.title,
+ content: data.content,
+ type: data.type || 'CLIP',
+ url: data.url,
+ };
+
+ if (notebookId) body.notebookId = notebookId;
+ if (data.fileUrl) body.fileUrl = data.fileUrl;
+ if (data.mimeType) body.mimeType = data.mimeType;
+ if (data.fileSize) body.fileSize = data.fileSize;
+
+ const response = await fetch(`${settings.host}/api/notes`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${token}`,
+ },
+ body: JSON.stringify(body),
+ });
+
+ if (!response.ok) {
+ const text = await response.text();
+ throw new Error(`${response.status}: ${text}`);
+ }
+
+ return response.json();
+}
+
+async function uploadImage(imageUrl) {
+ const token = await getToken();
+ const settings = await getSettings();
+
+ // Fetch the image
+ const imgResponse = await fetch(imageUrl);
+ const blob = await imgResponse.blob();
+
+ // Extract filename
+ let filename;
+ try {
+ const urlPath = new URL(imageUrl).pathname;
+ filename = urlPath.split('/').pop() || `image-${Date.now()}.jpg`;
+ } catch {
+ filename = `image-${Date.now()}.jpg`;
+ }
+
+ // Upload to rNotes
+ const formData = new FormData();
+ formData.append('file', blob, filename);
+
+ const response = await fetch(`${settings.host}/api/uploads`, {
+ method: 'POST',
+ headers: {
+ 'Authorization': `Bearer ${token}`,
+ },
+ body: formData,
+ });
+
+ if (!response.ok) {
+ const text = await response.text();
+ throw new Error(`Upload failed: ${response.status} ${text}`);
+ }
+
+ return response.json();
+}
+
+// --- Context Menu Handler ---
+
+chrome.contextMenus.onClicked.addListener(async (info, tab) => {
+ try {
+ switch (info.menuItemId) {
+ case 'clip-page': {
+ // Get page HTML
+ let content = '';
+ try {
+ const [result] = await chrome.scripting.executeScript({
+ target: { tabId: tab.id },
+ func: () => document.body.innerHTML,
+ });
+ content = result?.result || '';
+ } catch {
+ content = `
Clipped from ${tab.url}
`;
+ }
+
+ await createNote({
+ title: tab.title || 'Untitled Clip',
+ content: content,
+ type: 'CLIP',
+ url: tab.url,
+ });
+
+ showNotification('Page Clipped', `"${tab.title}" saved to rNotes`);
+ break;
+ }
+
+ case 'save-link': {
+ const linkUrl = info.linkUrl;
+ const linkText = info.selectionText || linkUrl;
+
+ await createNote({
+ title: linkText,
+ content: `${linkText}
Found on: ${tab.title}
`,
+ type: 'BOOKMARK',
+ url: linkUrl,
+ });
+
+ showNotification('Link Saved', `Bookmark saved to rNotes`);
+ break;
+ }
+
+ case 'save-image': {
+ const imageUrl = info.srcUrl;
+
+ // Upload the image first
+ const upload = await uploadImage(imageUrl);
+
+ // Create IMAGE note with file reference
+ await createNote({
+ title: `Image from ${tab.title || 'page'}`,
+ content: `
Source: ${tab.title}
`,
+ type: 'IMAGE',
+ url: tab.url,
+ fileUrl: upload.url,
+ mimeType: upload.mimeType,
+ fileSize: upload.size,
+ });
+
+ showNotification('Image Saved', `Image saved to rNotes`);
+ break;
+ }
+
+ case 'clip-selection': {
+ // Get selection HTML
+ let content = '';
+ try {
+ const [result] = await chrome.scripting.executeScript({
+ target: { tabId: tab.id },
+ func: () => {
+ const selection = window.getSelection();
+ if (!selection || selection.rangeCount === 0) return '';
+ const range = selection.getRangeAt(0);
+ const div = document.createElement('div');
+ div.appendChild(range.cloneContents());
+ return div.innerHTML;
+ },
+ });
+ content = result?.result || '';
+ } catch {
+ content = `${info.selectionText || ''}
`;
+ }
+
+ if (!content && info.selectionText) {
+ content = `${info.selectionText}
`;
+ }
+
+ await createNote({
+ title: `Selection from ${tab.title || 'page'}`,
+ content: content,
+ type: 'CLIP',
+ url: tab.url,
+ });
+
+ showNotification('Selection Clipped', `Saved to rNotes`);
+ break;
+ }
+ }
+ } catch (err) {
+ console.error('Context menu action failed:', err);
+ showNotification('rNotes Error', err.message || 'Failed to save');
+ }
+});
+
+// --- Message Handler (from popup) ---
+
+chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
+ if (message.type === 'notify') {
+ showNotification(message.title, message.message);
+ }
+});
diff --git a/browser-extension/icons/icon-128.png b/browser-extension/icons/icon-128.png
new file mode 100644
index 0000000..1e296f9
Binary files /dev/null and b/browser-extension/icons/icon-128.png differ
diff --git a/browser-extension/icons/icon-16.png b/browser-extension/icons/icon-16.png
new file mode 100644
index 0000000..62b0620
Binary files /dev/null and b/browser-extension/icons/icon-16.png differ
diff --git a/browser-extension/icons/icon-48.png b/browser-extension/icons/icon-48.png
new file mode 100644
index 0000000..3851e82
Binary files /dev/null and b/browser-extension/icons/icon-48.png differ
diff --git a/browser-extension/manifest.json b/browser-extension/manifest.json
new file mode 100644
index 0000000..315fc00
--- /dev/null
+++ b/browser-extension/manifest.json
@@ -0,0 +1,37 @@
+{
+ "manifest_version": 3,
+ "name": "rNotes Web Clipper",
+ "version": "1.0.0",
+ "description": "Clip pages, text, links, and images to rNotes.online",
+ "permissions": [
+ "activeTab",
+ "contextMenus",
+ "storage",
+ "notifications"
+ ],
+ "host_permissions": [
+ "https://rnotes.online/*",
+ "https://encryptid.jeffemmett.com/*",
+ "*://*/*"
+ ],
+ "action": {
+ "default_popup": "popup.html",
+ "default_icon": {
+ "16": "icons/icon-16.png",
+ "48": "icons/icon-48.png",
+ "128": "icons/icon-128.png"
+ }
+ },
+ "icons": {
+ "16": "icons/icon-16.png",
+ "48": "icons/icon-48.png",
+ "128": "icons/icon-128.png"
+ },
+ "background": {
+ "service_worker": "background.js"
+ },
+ "options_ui": {
+ "page": "options.html",
+ "open_in_tab": false
+ }
+}
diff --git a/browser-extension/options.html b/browser-extension/options.html
new file mode 100644
index 0000000..9946840
--- /dev/null
+++ b/browser-extension/options.html
@@ -0,0 +1,231 @@
+
+
+
+
+
+
+
+ rNotes Web Clipper Settings
+
+
+
+
Connection
+
+
+
+
The URL of your rNotes instance
+
+
+
+
+
+
Authentication
+
+ Not signed in
+
+
+
+
+
+
+
Opens rNotes in a new tab. Sign in with your passkey.
+
+
+
+
+
After signing in, copy the extension token and paste it here.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Default Notebook
+
+
+
+
Pre-selected notebook when clipping
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/browser-extension/options.js b/browser-extension/options.js
new file mode 100644
index 0000000..55858c5
--- /dev/null
+++ b/browser-extension/options.js
@@ -0,0 +1,179 @@
+const DEFAULT_HOST = 'https://rnotes.online';
+
+// --- Helpers ---
+
+function decodeToken(token) {
+ try {
+ const payload = JSON.parse(atob(token.split('.')[1]));
+ if (payload.exp && payload.exp * 1000 < Date.now()) {
+ return null;
+ }
+ return payload;
+ } catch {
+ return null;
+ }
+}
+
+function showStatus(message, type) {
+ const el = document.getElementById('status');
+ el.textContent = message;
+ el.className = `status ${type}`;
+ if (type === 'success') {
+ setTimeout(() => { el.className = 'status'; }, 3000);
+ }
+}
+
+// --- Auth UI ---
+
+async function updateAuthUI() {
+ const { encryptid_token } = await chrome.storage.local.get(['encryptid_token']);
+ const claims = encryptid_token ? decodeToken(encryptid_token) : null;
+
+ const authStatus = document.getElementById('authStatus');
+ const loginSection = document.getElementById('loginSection');
+ const loggedInSection = document.getElementById('loggedInSection');
+
+ if (claims) {
+ const username = claims.username || claims.sub?.slice(0, 20) || 'Authenticated';
+ authStatus.textContent = `Signed in as ${username}`;
+ authStatus.className = 'auth-status authed';
+ loginSection.style.display = 'none';
+ loggedInSection.style.display = 'block';
+ } else {
+ authStatus.textContent = 'Not signed in';
+ authStatus.className = 'auth-status not-authed';
+ loginSection.style.display = 'block';
+ loggedInSection.style.display = 'none';
+ }
+}
+
+async function populateNotebooks() {
+ const { encryptid_token } = await chrome.storage.local.get(['encryptid_token']);
+ if (!encryptid_token) return;
+
+ const host = document.getElementById('host').value.replace(/\/+$/, '') || DEFAULT_HOST;
+
+ try {
+ const response = await fetch(`${host}/api/notebooks`, {
+ headers: { 'Authorization': `Bearer ${encryptid_token}` },
+ });
+
+ if (!response.ok) return;
+
+ const notebooks = await response.json();
+ const select = document.getElementById('defaultNotebook');
+
+ // Clear existing options (keep first)
+ while (select.options.length > 1) {
+ select.remove(1);
+ }
+
+ for (const nb of notebooks) {
+ const option = document.createElement('option');
+ option.value = nb.id;
+ option.textContent = nb.title;
+ select.appendChild(option);
+ }
+
+ // Restore saved default
+ const { lastNotebookId } = await chrome.storage.local.get(['lastNotebookId']);
+ if (lastNotebookId) {
+ select.value = lastNotebookId;
+ }
+ } catch (err) {
+ console.error('Failed to load notebooks:', err);
+ }
+}
+
+// --- Load settings ---
+
+async function loadSettings() {
+ const result = await chrome.storage.sync.get(['rnotesHost']);
+ document.getElementById('host').value = result.rnotesHost || DEFAULT_HOST;
+
+ await updateAuthUI();
+ await populateNotebooks();
+}
+
+// --- Event handlers ---
+
+// Open rNotes sign-in
+document.getElementById('openSigninBtn').addEventListener('click', () => {
+ const host = document.getElementById('host').value.replace(/\/+$/, '') || DEFAULT_HOST;
+ chrome.tabs.create({ url: `${host}/auth/signin?extension=true` });
+});
+
+// Save token
+document.getElementById('saveTokenBtn').addEventListener('click', async () => {
+ const tokenInput = document.getElementById('tokenInput').value.trim();
+
+ if (!tokenInput) {
+ showStatus('Please paste a token', 'error');
+ return;
+ }
+
+ const claims = decodeToken(tokenInput);
+ if (!claims) {
+ showStatus('Invalid or expired token', 'error');
+ return;
+ }
+
+ await chrome.storage.local.set({ encryptid_token: tokenInput });
+ document.getElementById('tokenInput').value = '';
+
+ showStatus(`Signed in as ${claims.username || claims.sub}`, 'success');
+ await updateAuthUI();
+ await populateNotebooks();
+});
+
+// Logout
+document.getElementById('logoutBtn').addEventListener('click', async () => {
+ await chrome.storage.local.remove(['encryptid_token']);
+ showStatus('Signed out', 'success');
+ await updateAuthUI();
+});
+
+// Save settings
+document.getElementById('saveBtn').addEventListener('click', async () => {
+ const host = document.getElementById('host').value.trim().replace(/\/+$/, '');
+ const notebookId = document.getElementById('defaultNotebook').value;
+
+ await chrome.storage.sync.set({ rnotesHost: host || DEFAULT_HOST });
+ await chrome.storage.local.set({ lastNotebookId: notebookId });
+
+ showStatus('Settings saved', 'success');
+});
+
+// Test connection
+document.getElementById('testBtn').addEventListener('click', async () => {
+ const host = document.getElementById('host').value.trim().replace(/\/+$/, '') || DEFAULT_HOST;
+ const { encryptid_token } = await chrome.storage.local.get(['encryptid_token']);
+
+ try {
+ const headers = {};
+ if (encryptid_token) {
+ headers['Authorization'] = `Bearer ${encryptid_token}`;
+ }
+
+ const response = await fetch(`${host}/api/notebooks`, { headers });
+
+ if (response.ok) {
+ const data = await response.json();
+ showStatus(`Connected! Found ${data.length || 0} notebooks.`, 'success');
+ } else if (response.status === 401) {
+ showStatus('Connected but not authenticated. Sign in first.', 'error');
+ } else {
+ showStatus(`Connection failed: ${response.status}`, 'error');
+ }
+ } catch (err) {
+ showStatus(`Cannot connect: ${err.message}`, 'error');
+ }
+});
+
+// Default notebook change
+document.getElementById('defaultNotebook').addEventListener('change', async (e) => {
+ await chrome.storage.local.set({ lastNotebookId: e.target.value });
+});
+
+// Init
+document.addEventListener('DOMContentLoaded', loadSettings);
diff --git a/browser-extension/popup.html b/browser-extension/popup.html
new file mode 100644
index 0000000..d0db5ac
--- /dev/null
+++ b/browser-extension/popup.html
@@ -0,0 +1,223 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/browser-extension/popup.js b/browser-extension/popup.js
new file mode 100644
index 0000000..c0fa35d
--- /dev/null
+++ b/browser-extension/popup.js
@@ -0,0 +1,269 @@
+const DEFAULT_HOST = 'https://rnotes.online';
+
+let currentTab = null;
+let selectedText = '';
+let selectedHtml = '';
+
+// --- Helpers ---
+
+async function getSettings() {
+ const result = await chrome.storage.sync.get(['rnotesHost']);
+ return {
+ host: result.rnotesHost || DEFAULT_HOST,
+ };
+}
+
+async function getToken() {
+ const result = await chrome.storage.local.get(['encryptid_token']);
+ return result.encryptid_token || null;
+}
+
+function decodeToken(token) {
+ try {
+ const payload = JSON.parse(atob(token.split('.')[1]));
+ // Check expiry
+ if (payload.exp && payload.exp * 1000 < Date.now()) {
+ return null; // expired
+ }
+ return payload;
+ } catch {
+ return null;
+ }
+}
+
+function parseTags(tagString) {
+ if (!tagString || !tagString.trim()) return [];
+ return tagString.split(',').map(t => t.trim().toLowerCase()).filter(Boolean);
+}
+
+function showStatus(message, type) {
+ const el = document.getElementById('status');
+ el.textContent = message;
+ el.className = `status ${type}`;
+ if (type === 'success') {
+ setTimeout(() => { el.className = 'status'; }, 3000);
+ }
+}
+
+// --- API calls ---
+
+async function createNote(data) {
+ const token = await getToken();
+ const settings = await getSettings();
+
+ const body = {
+ title: data.title,
+ content: data.content,
+ type: data.type || 'CLIP',
+ url: data.url,
+ };
+
+ const notebookId = document.getElementById('notebook').value;
+ if (notebookId) body.notebookId = notebookId;
+
+ const tags = parseTags(document.getElementById('tags').value);
+ if (tags.length > 0) body.tags = tags;
+
+ const response = await fetch(`${settings.host}/api/notes`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${token}`,
+ },
+ body: JSON.stringify(body),
+ });
+
+ if (!response.ok) {
+ const text = await response.text();
+ throw new Error(`${response.status}: ${text}`);
+ }
+
+ return response.json();
+}
+
+async function fetchNotebooks() {
+ const token = await getToken();
+ const settings = await getSettings();
+
+ const response = await fetch(`${settings.host}/api/notebooks`, {
+ headers: {
+ 'Authorization': `Bearer ${token}`,
+ },
+ });
+
+ if (!response.ok) return [];
+ const data = await response.json();
+ return Array.isArray(data) ? data : [];
+}
+
+// --- UI ---
+
+async function populateNotebooks() {
+ const select = document.getElementById('notebook');
+ try {
+ const notebooks = await fetchNotebooks();
+ // Keep the "No notebook" option
+ for (const nb of notebooks) {
+ const option = document.createElement('option');
+ option.value = nb.id;
+ option.textContent = nb.title;
+ select.appendChild(option);
+ }
+
+ // Restore last used notebook
+ const { lastNotebookId } = await chrome.storage.local.get(['lastNotebookId']);
+ if (lastNotebookId) {
+ select.value = lastNotebookId;
+ }
+ } catch (err) {
+ console.error('Failed to load notebooks:', err);
+ }
+}
+
+// Save last used notebook when changed
+function setupNotebookMemory() {
+ document.getElementById('notebook').addEventListener('change', (e) => {
+ chrome.storage.local.set({ lastNotebookId: e.target.value });
+ });
+}
+
+async function init() {
+ // Get current tab
+ const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
+ currentTab = tab;
+
+ // Display page info
+ document.getElementById('pageTitle').textContent = tab.title || 'Untitled';
+ document.getElementById('pageUrl').textContent = tab.url || '';
+
+ // Check auth
+ const token = await getToken();
+ const claims = token ? decodeToken(token) : null;
+
+ if (!claims) {
+ document.getElementById('userStatus').textContent = 'Not signed in';
+ document.getElementById('userStatus').classList.add('not-authed');
+ document.getElementById('authWarning').style.display = 'block';
+ return;
+ }
+
+ document.getElementById('userStatus').textContent = claims.username || claims.sub?.slice(0, 16) || 'Authenticated';
+ document.getElementById('authWarning').style.display = 'none';
+
+ // Enable buttons
+ document.getElementById('clipPageBtn').disabled = false;
+
+ // Load notebooks
+ await populateNotebooks();
+ setupNotebookMemory();
+
+ // Detect text selection
+ try {
+ const [result] = await chrome.scripting.executeScript({
+ target: { tabId: tab.id },
+ func: () => {
+ const selection = window.getSelection();
+ if (!selection || selection.rangeCount === 0 || selection.isCollapsed) {
+ return { text: '', html: '' };
+ }
+ const range = selection.getRangeAt(0);
+ const div = document.createElement('div');
+ div.appendChild(range.cloneContents());
+ return { text: selection.toString(), html: div.innerHTML };
+ },
+ });
+
+ if (result?.result?.text) {
+ selectedText = result.result.text;
+ selectedHtml = result.result.html;
+ document.getElementById('clipSelectionBtn').disabled = false;
+ }
+ } catch (err) {
+ // Can't access some pages (chrome://, etc.)
+ console.warn('Cannot access page content:', err);
+ }
+}
+
+// --- Event handlers ---
+
+document.getElementById('clipPageBtn').addEventListener('click', async () => {
+ const btn = document.getElementById('clipPageBtn');
+ btn.disabled = true;
+ showStatus('Clipping page...', 'loading');
+
+ try {
+ // Get page HTML content
+ let pageContent = '';
+ try {
+ const [result] = await chrome.scripting.executeScript({
+ target: { tabId: currentTab.id },
+ func: () => document.body.innerHTML,
+ });
+ pageContent = result?.result || '';
+ } catch {
+ // Fallback: just use URL as content
+ pageContent = `Clipped from ${currentTab.url}
`;
+ }
+
+ const note = await createNote({
+ title: currentTab.title || 'Untitled Clip',
+ content: pageContent,
+ type: 'CLIP',
+ url: currentTab.url,
+ });
+
+ showStatus(`Clipped! Note saved.`, 'success');
+
+ // Notify background worker
+ chrome.runtime.sendMessage({
+ type: 'notify',
+ title: 'Page Clipped',
+ message: `"${currentTab.title}" saved to rNotes`,
+ });
+ } catch (err) {
+ showStatus(`Error: ${err.message}`, 'error');
+ } finally {
+ btn.disabled = false;
+ }
+});
+
+document.getElementById('clipSelectionBtn').addEventListener('click', async () => {
+ const btn = document.getElementById('clipSelectionBtn');
+ btn.disabled = true;
+ showStatus('Clipping selection...', 'loading');
+
+ try {
+ const content = selectedHtml || `${selectedText}
`;
+ const note = await createNote({
+ title: `Selection from ${currentTab.title || 'page'}`,
+ content: content,
+ type: 'CLIP',
+ url: currentTab.url,
+ });
+
+ showStatus(`Selection clipped!`, 'success');
+
+ chrome.runtime.sendMessage({
+ type: 'notify',
+ title: 'Selection Clipped',
+ message: `Saved to rNotes`,
+ });
+ } catch (err) {
+ showStatus(`Error: ${err.message}`, 'error');
+ } finally {
+ btn.disabled = false;
+ }
+});
+
+document.getElementById('optionsLink').addEventListener('click', (e) => {
+ e.preventDefault();
+ chrome.runtime.openOptionsPage();
+});
+
+document.getElementById('openSettings')?.addEventListener('click', (e) => {
+ e.preventDefault();
+ chrome.runtime.openOptionsPage();
+});
+
+// Init on load
+document.addEventListener('DOMContentLoaded', init);
diff --git a/src/app/auth/signin/page.tsx b/src/app/auth/signin/page.tsx
index 435c8d6..7b4c172 100644
--- a/src/app/auth/signin/page.tsx
+++ b/src/app/auth/signin/page.tsx
@@ -9,26 +9,28 @@ function SignInForm() {
const router = useRouter();
const searchParams = useSearchParams();
const returnUrl = searchParams.get('returnUrl') || '/';
+ const isExtension = searchParams.get('extension') === 'true';
const { isAuthenticated, loading: authLoading, login, register } = useEncryptID();
const [mode, setMode] = useState<'signin' | 'register'>('signin');
const [username, setUsername] = useState('');
const [error, setError] = useState('');
const [busy, setBusy] = useState(false);
+ const [tokenCopied, setTokenCopied] = useState(false);
- // Redirect if already authenticated
+ // Redirect if already authenticated (skip if extension mode — show token instead)
useEffect(() => {
- if (isAuthenticated && !authLoading) {
+ if (isAuthenticated && !authLoading && !isExtension) {
router.push(returnUrl);
}
- }, [isAuthenticated, authLoading, router, returnUrl]);
+ }, [isAuthenticated, authLoading, router, returnUrl, isExtension]);
const handleSignIn = async () => {
setError('');
setBusy(true);
try {
await login();
- router.push(returnUrl);
+ if (!isExtension) router.push(returnUrl);
} catch (err) {
setError(err instanceof Error ? err.message : 'Sign in failed. Make sure you have a registered passkey.');
} finally {
@@ -45,7 +47,7 @@ function SignInForm() {
setBusy(true);
try {
await register(username.trim());
- router.push(returnUrl);
+ if (!isExtension) router.push(returnUrl);
} catch (err) {
setError(err instanceof Error ? err.message : 'Registration failed.');
} finally {
@@ -162,6 +164,33 @@ function SignInForm() {
)}
+ {/* Extension token display */}
+ {isExtension && isAuthenticated && (
+
+
Extension Token
+
+ Copy this token and paste it in the rNotes Web Clipper extension settings.
+
+
+ )}
+
Powered by EncryptID — passwordless, decentralized identity