Skip to content

Commit 6562fef

Browse files
committed
Easy pairing for chrome extension
1 parent 8af6660 commit 6562fef

File tree

5 files changed

+136
-3
lines changed

5 files changed

+136
-3
lines changed

extension/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@
1212
3. Set the API base URL (default: `http://127.0.0.1:8000`)
1313
4. Set your **Workspace token** (use the same token as the web app)
1414

15+
## Auto-pairing (recommended)
16+
On the web app’s token page (`/#/onboarding/token`), click **Pair extension** to send the
17+
workspace token to the extension automatically.
18+
1519
Note: the extension does **not** read `.env` files. Its API base URL is stored in Chrome sync storage
1620
and can differ from the frontend’s `VITE_API_BASE_URL` if needed.
1721

extension/background.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,12 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
5252
if (!message || !message.type) return
5353

5454
;(async () => {
55+
if (message.type === 'EASYRELOCATE_SET_WORKSPACE_TOKEN') {
56+
const token = String(message.token || '').trim()
57+
await setWorkspaceToken(token)
58+
sendResponse({ ok: true })
59+
return
60+
}
5561
const path =
5662
message.type === 'EASYRELOCATE_ADD_LISTING_FROM_TEXT'
5763
? '/api/listings/from_text'

extension/manifest.json

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
"description": "Save listings (Airbnb, Blueground) and selected posts to compare in EasyRelocate.",
66
"permissions": ["storage", "contextMenus"],
77
"host_permissions": [
8-
"http://localhost/*",
9-
"http://127.0.0.1/*",
8+
"https://easyrelocate.net/*",
9+
"https://www.easyrelocate.net/*",
1010
"https://airbnb.com/*",
1111
"https://*.airbnb.com/*",
1212
"https://theblueground.com/*",
@@ -18,6 +18,14 @@
1818
"service_worker": "background.js"
1919
},
2020
"content_scripts": [
21+
{
22+
"matches": [
23+
"https://easyrelocate.net/*",
24+
"https://www.easyrelocate.net/*"
25+
],
26+
"js": ["shared/pairing.js"],
27+
"run_at": "document_idle"
28+
},
2129
{
2230
"matches": ["https://airbnb.com/rooms/*", "https://*.airbnb.com/rooms/*"],
2331
"js": ["shared/overlay.js", "platforms/airbnb/content.js"],

extension/shared/pairing.js

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
;(function () {
2+
const REQUEST_TYPE = 'EASYRELOCATE_PAIR_REQUEST'
3+
const RESULT_TYPE = 'EASYRELOCATE_PAIR_RESULT'
4+
const PAIR_HASH = '/onboarding/token'
5+
6+
function shouldHandle() {
7+
return typeof location !== 'undefined' && location.hash.includes(PAIR_HASH)
8+
}
9+
10+
window.addEventListener('message', (event) => {
11+
if (event.source !== window) return
12+
if (!shouldHandle()) return
13+
if (event.origin !== window.location.origin) return
14+
15+
const data = event.data || {}
16+
if (data.type !== REQUEST_TYPE) return
17+
18+
const token = String(data.token || '').trim()
19+
if (!token) {
20+
window.postMessage(
21+
{ type: RESULT_TYPE, ok: false, error: 'Missing token' },
22+
window.location.origin
23+
)
24+
return
25+
}
26+
27+
try {
28+
chrome.runtime.sendMessage(
29+
{ type: 'EASYRELOCATE_SET_WORKSPACE_TOKEN', token },
30+
(response) => {
31+
const err = chrome.runtime.lastError
32+
if (err) {
33+
window.postMessage(
34+
{ type: RESULT_TYPE, ok: false, error: err.message || String(err) },
35+
window.location.origin
36+
)
37+
return
38+
}
39+
if (!response?.ok) {
40+
window.postMessage(
41+
{ type: RESULT_TYPE, ok: false, error: response?.error || 'Pairing failed' },
42+
window.location.origin
43+
)
44+
return
45+
}
46+
window.postMessage({ type: RESULT_TYPE, ok: true }, window.location.origin)
47+
}
48+
)
49+
} catch (e) {
50+
window.postMessage(
51+
{ type: RESULT_TYPE, ok: false, error: e?.message || String(e) },
52+
window.location.origin
53+
)
54+
}
55+
})
56+
})()

frontend/src/pages/OnboardingTokenPage.tsx

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import '../App.css'
22

3-
import { useEffect, useMemo, useState } from 'react'
3+
import { useEffect, useMemo, useRef, useState } from 'react'
44
import { Link, useNavigate } from 'react-router-dom'
55

66
import { issueWorkspaceToken, type WorkspaceIssue } from '../api'
@@ -31,9 +31,16 @@ export default function OnboardingTokenPage() {
3131
const [loading, setLoading] = useState(true)
3232
const [error, setError] = useState<string | null>(null)
3333
const [issued, setIssued] = useState<WorkspaceIssue | null>(null)
34+
const [pairStatus, setPairStatus] = useState<string | null>(null)
35+
const pairTimeoutRef = useRef<number | null>(null)
3436

3537
const token = issued?.workspace_token ?? ''
3638
const expiresAt = issued?.expires_at ?? ''
39+
const savedToken = useMemo(() => {
40+
const raw = localStorage.getItem('easyrelocate_workspace_token')
41+
return (raw ?? '').trim()
42+
}, [issued])
43+
const effectiveToken = token || savedToken
3744

3845
const alreadyHasToken = useMemo(() => {
3946
const raw = localStorage.getItem('easyrelocate_workspace_token')
@@ -96,6 +103,48 @@ export default function OnboardingTokenPage() {
96103
navigate('/compare')
97104
}
98105

106+
const onPairExtension = () => {
107+
const t = effectiveToken.trim()
108+
if (!t) {
109+
setPairStatus('Missing token to pair.')
110+
return
111+
}
112+
setPairStatus('Waiting for extension…')
113+
if (pairTimeoutRef.current) {
114+
window.clearTimeout(pairTimeoutRef.current)
115+
}
116+
pairTimeoutRef.current = window.setTimeout(() => {
117+
setPairStatus('No response from extension. Make sure it is installed and enabled.')
118+
}, 3000)
119+
window.postMessage({ type: 'EASYRELOCATE_PAIR_REQUEST', token: t }, window.location.origin)
120+
}
121+
122+
useEffect(() => {
123+
const handler = (event: MessageEvent) => {
124+
if (event.source !== window) return
125+
if (event.origin !== window.location.origin) return
126+
const data = event.data as { type?: string; ok?: boolean; error?: string } | null
127+
if (!data || data.type !== 'EASYRELOCATE_PAIR_RESULT') return
128+
if (pairTimeoutRef.current) {
129+
window.clearTimeout(pairTimeoutRef.current)
130+
pairTimeoutRef.current = null
131+
}
132+
if (data.ok) {
133+
setPairStatus('Extension paired successfully.')
134+
} else {
135+
setPairStatus(data.error || 'Failed to pair with extension.')
136+
}
137+
}
138+
window.addEventListener('message', handler)
139+
return () => {
140+
window.removeEventListener('message', handler)
141+
if (pairTimeoutRef.current) {
142+
window.clearTimeout(pairTimeoutRef.current)
143+
pairTimeoutRef.current = null
144+
}
145+
}
146+
}, [])
147+
99148
return (
100149
<div className="landing">
101150
<header className="landingHeader">
@@ -173,6 +222,9 @@ export default function OnboardingTokenPage() {
173222
<button className="button secondary" onClick={() => void onCopy()}>
174223
Copy
175224
</button>
225+
<button className="button secondary" onClick={onPairExtension}>
226+
Pair extension
227+
</button>
176228
<button className="button" onClick={onContinue}>
177229
Continue to map
178230
</button>
@@ -185,11 +237,18 @@ export default function OnboardingTokenPage() {
185237
<button className="button secondary" onClick={() => void doIssue()}>
186238
Generate token
187239
</button>
240+
<button className="button secondary" onClick={onPairExtension}>
241+
Pair extension
242+
</button>
188243
<button className="button" onClick={onContinue}>
189244
Continue to map
190245
</button>
191246
</div>
192247
) : null}
248+
249+
{pairStatus ? (
250+
<div style={{ marginTop: 12, color: '#475569', fontSize: 13 }}>{pairStatus}</div>
251+
) : null}
193252
</div>
194253
</section>
195254
</main>

0 commit comments

Comments
 (0)