window.nostr.js 0.3.0 → 0.4.1

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.
@@ -0,0 +1,9 @@
1
+ import {build} from 'esbuild'
2
+
3
+ build({
4
+ entryPoints: ['iframe/iframe.ts'],
5
+ bundle: true,
6
+ sourcemap: 'external',
7
+ outfile: 'dist/iframe.js',
8
+ format: 'iife'
9
+ }).then(() => console.log('iframe built'))
@@ -0,0 +1,28 @@
1
+ const SHARED_BUNKER_KEY = 'wnj:root:sharedBunker'
2
+
3
+ window.onmessage = async (ev: MessageEvent) => {
4
+ const {bunker, getbunker} = ev.data
5
+
6
+ // set the bunker URL
7
+ if (bunker) {
8
+ // we only accept these websites to be setting the bunker URL
9
+ if (
10
+ !ev.origin.startsWith('https://join.the-nostr.org') &&
11
+ !ev.origin.startsWith('http://localhost:6711')
12
+ ) {
13
+ return
14
+ }
15
+
16
+ if (bunker) {
17
+ localStorage.setItem(SHARED_BUNKER_KEY, bunker)
18
+ } else {
19
+ console.error('wnj iframe got an invalid bunker url:', bunker)
20
+ }
21
+ }
22
+
23
+ if (getbunker) {
24
+ // any website can get the bunker URL if it is set
25
+ const bunker = localStorage.getItem(SHARED_BUNKER_KEY)
26
+ window.parent.postMessage({bunker}, '*') // we reply even if it's undefined so they know
27
+ }
28
+ }
package/index.html CHANGED
@@ -15,7 +15,7 @@
15
15
  "
16
16
  >
17
17
  <main>
18
- <h1>App Title</h1>
18
+ <h1>Lorem Ipsum</h1>
19
19
  <p>
20
20
  Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed et ex dui.
21
21
  Donec eu dui sed massa auctor rhoncus. Nunc mollis, metus nec
@@ -34,31 +34,6 @@
34
34
  mollis sit amet. Cras sit amet mi sit amet eros convallis pulvinar.
35
35
  Donec condimentum condimentum tincidunt.
36
36
  </p>
37
-
38
- <p>
39
- Suspendisse eu ipsum vel lorem congue mattis in at neque. Phasellus nec
40
- felis sit amet nibh bibendum tristique a quis urna. Pellentesque laoreet
41
- porta libero eget pellentesque. Praesent tempor, nisi et efficitur
42
- cursus, leo nunc ullamcorper ante, quis eleifend velit justo sed enim.
43
- Maecenas laoreet dictum lorem rhoncus tempus. Integer ipsum augue,
44
- sagittis a facilisis et, mollis ut ipsum. Proin elementum consectetur
45
- tincidunt.
46
- </p>
47
-
48
- <p>
49
- Mauris nec mi et quam semper volutpat. Morbi suscipit felis lectus, in
50
- ornare purus pretium in. Maecenas et ex metus. Ut aliquam, felis vel
51
- mollis dapibus, tellus velit blandit dui, eu hendrerit tellus magna at
52
- eros. Donec scelerisque hendrerit auctor. Aenean aliquet sapien
53
- elementum dui sagittis eleifend. Donec hendrerit odio et dui lacinia
54
- aliquet. Morbi auctor ultrices dui fringilla elementum. Curabitur
55
- scelerisque lorem vitae placerat vulputate. Donec vestibulum urna sem,
56
- at sodales ligula laoreet id. Etiam vehicula, lorem id tincidunt
57
- sodales, sem odio vehicula ex, vel condimentum quam augue id augue.
58
- Integer a varius nunc. Mauris rhoncus leo sed facilisis bibendum. Nullam
59
- consectetur pellentesque vestibulum. Cras hendrerit feugiat orci, in
60
- molestie risus rutrum ut. Proin dictum nunc at rutrum vehicula.
61
- </p>
62
37
  </main>
63
38
  <!-- <script>
64
39
  window.wnjParams = {
package/justfile CHANGED
@@ -1,12 +1,18 @@
1
1
  export PATH := "./node_modules/.bin:" + env_var('PATH')
2
+ set unstable
2
3
 
3
4
  dev:
4
- vite
5
+ vite --port 13471
5
6
 
6
- build:
7
+ build: wnj iframe
8
+
9
+ wnj:
7
10
  vite build
8
11
  mv dist/assets/*.js dist/window.nostr.js
9
12
 
13
+ iframe:
14
+ node ./iframe/build.js
15
+
10
16
  demo:
11
17
  xdg-open demo/index.html
12
18
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "window.nostr.js",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
4
4
  "type": "module",
5
5
  "main": "dist/window.nostr.js",
6
6
  "devDependencies": {
@@ -8,6 +8,7 @@
8
8
  "@tsconfig/svelte": "^5.0.2",
9
9
  "@typescript-eslint/eslint-plugin": "^7.0.1",
10
10
  "autoprefixer": "^10.4.17",
11
+ "esbuild": "^0.24.0",
11
12
  "eslint-config-prettier": "^9.1.0",
12
13
  "eslint-plugin-svelte": "^2.35.1",
13
14
  "postcss": "^8.4.35",
@@ -22,7 +23,7 @@
22
23
  "vite": "^5.1.0"
23
24
  },
24
25
  "dependencies": {
25
- "debounce": "^2.0.0",
26
- "nostr-tools": "2.5.2"
26
+ "debounce": "2.0.0",
27
+ "nostr-tools": "^2.9.1"
27
28
  }
28
29
  }
package/src/App.svelte CHANGED
@@ -25,12 +25,14 @@
25
25
  import Spinner from './Spinner.svelte'
26
26
 
27
27
  const mobileMode = mediaQueryStore('only screen and (max-width: 640px)')
28
- const localStorageKeys = {
28
+ const lskeys = {
29
29
  ORIGIN: 'wnj:origin',
30
30
  CLIENT_SECRET: 'wnj:clientSecret',
31
31
  Y_POS: 'wnj:ypos',
32
32
  CALLBACK_TOKEN: 'wnj:callbackToken',
33
- BUNKER_POINTER: 'wnj:bunkerPointer'
33
+ BUNKER_POINTER: 'wnj:bunkerPointer',
34
+ CACHED_PUBKEY: 'wnj:cachedPubKey',
35
+ IGNORE_IFRAME: 'wnj:ignoreIframe'
34
36
  }
35
37
 
36
38
  let myself: HTMLDivElement
@@ -38,30 +40,27 @@
38
40
  export let position: 'top' | 'bottom' = 'top'
39
41
  $: origin = $mobileMode
40
42
  ? 'bottom'
41
- : (localStorage.getItem(localStorageKeys.ORIGIN) as
42
- | 'top'
43
- | 'bottom'
44
- | null) || position
43
+ : (localStorage.getItem(lskeys.ORIGIN) as 'top' | 'bottom' | null) ||
44
+ position
45
45
  export let startHidden: boolean
46
46
  export let compactMode: boolean
47
47
 
48
48
  const win = window as any
49
49
  const pool = new SimplePool()
50
+ let useIframe = false
51
+ let iframe: HTMLIFrameElement | undefined
50
52
  let bunkerInput: HTMLInputElement
51
53
  let bunkerInputValue: string
52
54
  let nameInput: HTMLInputElement
53
55
  let nameInputValue: string
54
56
  let chosenProvider: BunkerProfile | undefined
55
57
  let clientSecret: Uint8Array
56
- const local = localStorage.getItem(localStorageKeys.CLIENT_SECRET)
58
+ const local = localStorage.getItem(lskeys.CLIENT_SECRET)
57
59
  if (local) {
58
60
  clientSecret = hexToBytes(local)
59
61
  } else {
60
62
  clientSecret = generateSecretKey()
61
- localStorage.setItem(
62
- localStorageKeys.CLIENT_SECRET,
63
- bytesToHex(clientSecret)
64
- )
63
+ localStorage.setItem(lskeys.CLIENT_SECRET, bytesToHex(clientSecret))
65
64
  }
66
65
 
67
66
  let state: 'opened' | 'closed' | 'justopened' | 'justclosed' = 'closed'
@@ -97,7 +96,7 @@
97
96
  export let right = 20
98
97
  $: ypos = $mobileMode
99
98
  ? BASE_YPOS
100
- : parseInt(localStorage.getItem(localStorageKeys.Y_POS) || '0') || BASE_YPOS
99
+ : parseInt(localStorage.getItem(lskeys.Y_POS) || '0') || BASE_YPOS
101
100
  let dragStarted = false
102
101
  let hasMoved = false
103
102
  let insidePosition: number
@@ -122,7 +121,7 @@
122
121
  onauth(url: string) {
123
122
  if (creating) {
124
123
  showAuth = url
125
- } else if(identity) {
124
+ } else if (identity) {
126
125
  showConfirmAction = url
127
126
  opened = true
128
127
  } else {
@@ -169,9 +168,8 @@
169
168
  let windowNostr = {
170
169
  isWnj: true,
171
170
  async getPublicKey(): Promise<string> {
172
- if (bunkerPointer) return bunkerPointer.pubkey
173
171
  if (!connecting && !connected) open()
174
- return (await bunker).bp.pubkey
172
+ return (await bunker).getPublicKey()
175
173
  },
176
174
  async signEvent(event: NostrEvent): Promise<VerifiedEvent> {
177
175
  try {
@@ -218,12 +216,22 @@
218
216
 
219
217
  onMount(() => {
220
218
  if (!bunkerPointer) {
221
- let data = localStorage.getItem(localStorageKeys.BUNKER_POINTER)
219
+ let data = localStorage.getItem(lskeys.BUNKER_POINTER)
222
220
  if (data) {
223
221
  bunkerPointer = JSON.parse(data)
224
- // we have a pointer, which means we can get the public key right away
225
- // but we will only try to connect when any other method is called on window.nostr
226
222
  identify()
223
+
224
+ // we must connect here so identify() works because we can't rely on the bunker params to read our pubkey
225
+ // however we may first check if we have it cached locally before doing the expensive connection
226
+ let cachedPubkey = localStorage.getItem(lskeys.CACHED_PUBKEY)
227
+ if (!cachedPubkey) {
228
+ connect()
229
+ }
230
+ } else {
231
+ // when we don't have any bunker data stored, we can still check the iframe for it
232
+ if (!localStorage.getItem(lskeys.IGNORE_IFRAME)) {
233
+ useIframe = true
234
+ }
227
235
  }
228
236
  }
229
237
 
@@ -256,6 +264,21 @@
256
264
  }
257
265
  })
258
266
 
267
+ function onIframeLoaded() {
268
+ window.addEventListener('message', handleMessage)
269
+ iframe?.contentWindow?.postMessage({getbunker: true}, '*')
270
+
271
+ async function handleMessage(ev: MessageEvent) {
272
+ let {bunker} = ev.data
273
+ if (bunker) {
274
+ bunkerPointer = await parseBunkerInput(bunker)
275
+ identify()
276
+ connect()
277
+ window.removeEventListener('message', handleMessage)
278
+ }
279
+ }
280
+ }
281
+
259
282
  function handleClick(ev: MouseEvent) {
260
283
  if (Math.abs(ypos - yposStart) > 6 || Date.now() - clickStart > 600) {
261
284
  return
@@ -318,7 +341,9 @@
318
341
 
319
342
  async function handleDisconnect(ev: MouseEvent) {
320
343
  ev.preventDefault()
321
- localStorage.removeItem(localStorageKeys.BUNKER_POINTER)
344
+ localStorage.removeItem(lskeys.BUNKER_POINTER)
345
+ localStorage.removeItem(lskeys.CACHED_PUBKEY)
346
+ localStorage.setItem(lskeys.IGNORE_IFRAME, '')
322
347
  reset()
323
348
  }
324
349
 
@@ -391,10 +416,7 @@
391
416
  try {
392
417
  await b.connect()
393
418
  connected = true
394
- localStorage.setItem(
395
- localStorageKeys.BUNKER_POINTER,
396
- JSON.stringify(bunkerPointer)
397
- )
419
+ localStorage.setItem(lskeys.BUNKER_POINTER, JSON.stringify(bunkerPointer))
398
420
  close()
399
421
  resolveBunker(b)
400
422
  } catch (err: any) {
@@ -410,8 +432,16 @@
410
432
  }
411
433
  }
412
434
 
413
- function identify(onFirstMetadata: (() => void) | null = null) {
414
- let pubkey = bunkerPointer!.pubkey
435
+ // identify() is what gives a name and picture to our floating widget
436
+ async function identify() {
437
+ let pubkey = localStorage.getItem(lskeys.CACHED_PUBKEY)
438
+ if (!pubkey) {
439
+ pubkey = await (await bunker).getPublicKey()
440
+
441
+ // store this pubkey here so we don't have to connect and get our pubkey immediately
442
+ // the next time we open this page
443
+ localStorage.setItem(lskeys.CACHED_PUBKEY, pubkey)
444
+ }
415
445
 
416
446
  identity = {
417
447
  pubkey: pubkey,
@@ -434,8 +464,6 @@
434
464
  identity!.event = evt
435
465
  identity!.name = name
436
466
  identity!.picture = picture
437
- onFirstMetadata?.()
438
- onFirstMetadata = null
439
467
  } catch (err) {
440
468
  /***/
441
469
  }
@@ -497,8 +525,8 @@
497
525
  ypos = BASE_YPOS
498
526
  }
499
527
 
500
- localStorage.setItem(localStorageKeys.ORIGIN, origin)
501
- localStorage.setItem(localStorageKeys.Y_POS, ypos.toString())
528
+ localStorage.setItem(lskeys.ORIGIN, origin)
529
+ localStorage.setItem(lskeys.Y_POS, ypos.toString())
502
530
  }
503
531
  }
504
532
  </script>
@@ -532,18 +560,18 @@
532
560
  >
533
561
  <!-- Connecting view ################### -->
534
562
  {#if connecting}
535
- <div class="flex px-2 items-center">
563
+ <div class="flex items-center px-2">
536
564
  Connecting to bunker
537
565
  <Spinner />
538
566
  </div>
539
- {:else if !identity && !compactMode}
540
- <div class="flex px-2 items-center">
541
- Connect with Nostr
542
- </div>
543
- {:else if !identity && compactMode}
544
- <div class="w-6 text-center">N</div>
567
+ {:else if !identity}
568
+ {#if compactMode}
569
+ <div class="w-6 text-center">N</div>
570
+ {:else}
571
+ <div class="flex items-center px-2">Connect with Nostr</div>
572
+ {/if}
545
573
  {:else if !compactMode}
546
- <div class="flex px-2 items-center">
574
+ <div class="flex items-center px-2">
547
575
  {#if identity.picture}
548
576
  <img
549
577
  src={identity.picture}
@@ -551,19 +579,17 @@
551
579
  class="mr-2 h-5 w-5 rounded-full"
552
580
  />
553
581
  {:else}
554
-
582
+ <span class="mr-2">☉</span>
555
583
  {/if}
556
- <div class="max-w-56 overflow-hidden whitespace-nowrap overflow-ellipsis inline-block">
584
+ <div
585
+ class="inline-block max-w-56 overflow-hidden overflow-ellipsis whitespace-nowrap"
586
+ >
557
587
  {identity.name ||
558
588
  identity.npub.slice(0, 7) + '…' + identity.npub.slice(-4)}
559
589
  </div>
560
590
  </div>
561
591
  {:else}
562
- <img
563
- src={identity.picture}
564
- alt=""
565
- class="h-6 w-6 rounded-full"
566
- />
592
+ <img src={identity.picture} alt="" class="h-6 w-6 rounded-full" />
567
593
  {/if}
568
594
  </div>
569
595
 
@@ -590,40 +616,57 @@
590
616
  <div class="m-auto w-full">
591
617
  <div class="text-center text-lg">Create a Nostr account</div>
592
618
  <div class="mt-4 text-center text-sm leading-4">
593
- Now you a new window will bring you to <strong>{new URL(showAuth).host}</strong> where the account creation will take place. If nothing happens check that if your browser is blocking popups, pleaase.<br/>
619
+ Now you a new window will bring you to <strong
620
+ >{new URL(showAuth).host}</strong
621
+ >
622
+ where the account creation will take place. If nothing happens check
623
+ that if your browser is blocking popups, pleaase.<br />
594
624
  After that you will be returned to this page.
595
625
  </div>
596
626
  <button
597
627
  class="mt-4 block w-full cursor-pointer rounded border-0 px-2 py-1 text-lg text-white disabled:cursor-default disabled:bg-neutral-400 disabled:text-neutral-200 bg-{accent}-900 hover:bg-{accent}-950"
598
- on:click={() => openAuthURLPopup(showAuth)}>
628
+ on:click={() => openAuthURLPopup(showAuth)}
629
+ >
599
630
  Start account creation »
600
631
  </button>
601
632
  </div>
602
-
603
633
  {:else if showLogin}
604
634
  <div class="m-auto w-full">
605
635
  <div class="text-center text-lg">Login into a Nostr account</div>
606
636
  <div class="mt-4 text-center text-sm leading-4">
607
- Now you a new window will bring you to <strong>{new URL(showLogin).host}</strong> where you can login and approve the permissions. If nothing happens check that if your browser is blocking popups, pleaase.<br/>
637
+ Now you a new window will bring you to <strong
638
+ >{new URL(showLogin).host}</strong
639
+ >
640
+ where you can login and approve the permissions. If nothing happens check
641
+ that if your browser is blocking popups, pleaase.<br />
608
642
  After that you will be returned to this page.
609
643
  </div>
610
644
  <button
611
645
  class="mt-4 block w-full cursor-pointer rounded border-0 px-2 py-1 text-lg text-white disabled:cursor-default disabled:bg-neutral-400 disabled:text-neutral-200 bg-{accent}-900 hover:bg-{accent}-950"
612
- on:click={() => openAuthURLPopup(showLogin)}>
646
+ on:click={() => openAuthURLPopup(showLogin)}
647
+ >
613
648
  Login now »
614
649
  </button>
615
650
  </div>
616
-
617
- {:else if showConfirmAction}
651
+ {:else if showConfirmAction}
618
652
  <div class="m-auto w-full">
619
- <div class="text-center text-lg">An action requires your confirmation</div>
653
+ <div class="text-center text-lg">
654
+ An action requires your confirmation
655
+ </div>
620
656
  <div class="mt-4 text-center text-sm leading-4">
621
- Now you a new window will bring you to <strong>{new URL(showConfirmAction).host}</strong> where you can approve the current action. If nothing happens check that if your browser is blocking popups, pleaase.<br/>
657
+ Now you a new window will bring you to <strong
658
+ >{new URL(showConfirmAction).host}</strong
659
+ >
660
+ where you can approve the current action. If nothing happens check that
661
+ if your browser is blocking popups, pleaase.<br />
622
662
  After that you will be returned to this page.
623
663
  </div>
624
664
  <button
625
665
  class="mt-4 block w-full cursor-pointer rounded border-0 px-2 py-1 text-lg text-white disabled:cursor-default disabled:bg-neutral-400 disabled:text-neutral-200 bg-{accent}-900 hover:bg-{accent}-950"
626
- on:click={() => {openAuthURLPopup(showConfirmAction)}}>
666
+ on:click={() => {
667
+ openAuthURLPopup(showConfirmAction)
668
+ }}
669
+ >
627
670
  Confirm action »
628
671
  </button>
629
672
  </div>
@@ -797,3 +840,11 @@
797
840
  </div>
798
841
  {/if}
799
842
  </div>
843
+ {#if useIframe}
844
+ <iframe
845
+ title="~"
846
+ bind:this={iframe}
847
+ on:load={onIframeLoaded}
848
+ src="https://the-nostr.org/iframe.html"
849
+ ></iframe>
850
+ {/if}
package/src/main.ts CHANGED
@@ -37,24 +37,24 @@ const app = new App({
37
37
  props: {
38
38
  accent: win.wnjParams?.accent || 'cyan',
39
39
  position: win.wnjParams?.position === 'bottom' ? 'bottom' : 'top',
40
- startHidden: win.wnjParams?.startHidden ? true : false,
41
- compactMode: win.wnjParams?.compactMode ? true : false,
40
+ startHidden: win.wnjParams?.startHidden,
41
+ compactMode: win.wnjParams?.compactMode
42
42
  }
43
43
  })
44
44
 
45
- if (!win.wnjParams?.disableOverflowFix){
45
+ if (!win.wnjParams?.disableOverflowFix) {
46
46
  // Inject on the host page a style to avoid weird scrolling on the
47
47
  // right/bottom on mobile, if the underlying page has some horizontal
48
48
  // scrolling
49
- var styleElement = document.createElement('style');
50
- var cssCode = `
49
+ const styleElement = document.createElement('style')
50
+ const cssCode = `
51
51
  html, body {
52
52
  overflow: auto;
53
53
  height: 100%;
54
54
  }
55
- `;
56
- styleElement.innerHTML = cssCode;
57
- document.head.appendChild(styleElement);
55
+ `
56
+ styleElement.innerHTML = cssCode
57
+ document.head.appendChild(styleElement)
58
58
  }
59
59
 
60
60
  export default app
@@ -0,0 +1,25 @@
1
+ import {writable} from 'svelte/store'
2
+
3
+ export default (mediaQueryString: string) => {
4
+ const {subscribe, set} = writable<boolean>(undefined, () => {
5
+ // start observing media query
6
+ const mql = window.matchMedia(mediaQueryString)
7
+
8
+ // set first media query result to the store
9
+ set(mql.matches)
10
+
11
+ // called when media query state changes
12
+ const onchange = () => set(mql.matches)
13
+
14
+ // listen for changes (need to support old `addListener` interface)
15
+ mql.addEventListener('change', onchange)
16
+
17
+ // when no more listeners
18
+ return () => {
19
+ // stop listening (need to support old `removeListener` interface)
20
+ mql.removeEventListener('change', onchange)
21
+ }
22
+ })
23
+
24
+ return {subscribe}
25
+ }
package/tsconfig.json CHANGED
@@ -13,8 +13,9 @@
13
13
  */
14
14
  "allowJs": true,
15
15
  "checkJs": true,
16
- "isolatedModules": true
16
+ "isolatedModules": true,
17
+ "moduleDetection": "force"
17
18
  },
18
- "include": ["src/**/*.ts", "src/**/*.js", "src/**/*.svelte"],
19
- "references": [{ "path": "./tsconfig.node.json" }]
19
+ "include": ["src/**/*.ts", "src/**/*.js", "src/**/*.svelte", "iframe/*.ts"],
20
+ "references": [{"path": "./tsconfig.node.json"}]
20
21
  }
@@ -1,33 +0,0 @@
1
- import {writable} from 'svelte/store'
2
-
3
- /**
4
- * Svelte Media Query Store
5
- * @param mediaQueryString { string }
6
- */
7
- export default mediaQueryString => {
8
- const {subscribe, set} = writable(undefined, () => {
9
- // Start observing media query
10
- let mql = window.matchMedia(mediaQueryString)
11
-
12
- // Set first media query result to the store
13
- set(mql.matches)
14
-
15
- // Called when media query state changes
16
- const onchange = () => set(mql.matches)
17
-
18
- // Listen for changes (need to support old `addListener` interface)
19
- 'addEventListener' in mql
20
- ? mql.addEventListener('change', onchange)
21
- : mql.addListener(onchange)
22
-
23
- // when no more listeners
24
- return () => {
25
- // stop listening (need to support old `removeListener` interface)
26
- 'removeEventListener' in mql
27
- ? mql.removeEventListener('change', onchange)
28
- : mql.removeListener(onchange)
29
- }
30
- })
31
-
32
- return {subscribe}
33
- }