Merge branch 'release/3.8.22' into main
@@ -1,3 +1,3 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M17.5 7.50008H12.5V18.3334H10.8333V13.3334H9.16667V18.3334H7.5V7.50008H2.5V5.83342H17.5V7.50008ZM10 1.66675C10.442 1.66675 10.866 1.84234 11.1785 2.1549C11.4911 2.46746 11.6667 2.89139 11.6667 3.33342C11.6667 3.77544 11.4911 4.19937 11.1785 4.51193C10.866 4.82449 10.442 5.00008 10 5.00008C9.075 5.00008 8.33333 4.25008 8.33333 3.33342C8.33333 2.40841 9.075 1.66675 10 1.66675Z" fill="#72767E"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 508 B After Width: | Height: | Size: 485 B |
3
app/assets/icons/ic-account-circle.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10.0001 16.0001C7.91675 16.0001 6.07508 14.9334 5.00008 13.3334C5.02508 11.6667 8.33342 10.7501 10.0001 10.7501C11.6667 10.7501 14.9751 11.6667 15.0001 13.3334C13.9251 14.9334 12.0834 16.0001 10.0001 16.0001ZM10.0001 4.16675C10.6631 4.16675 11.299 4.43014 11.7679 4.89898C12.2367 5.36782 12.5001 6.00371 12.5001 6.66675C12.5001 7.32979 12.2367 7.96568 11.7679 8.43452C11.299 8.90336 10.6631 9.16675 10.0001 9.16675C9.33704 9.16675 8.70116 8.90336 8.23232 8.43452C7.76347 7.96568 7.50008 7.32979 7.50008 6.66675C7.50008 6.00371 7.76347 5.36782 8.23232 4.89898C8.70116 4.43014 9.33704 4.16675 10.0001 4.16675ZM10.0001 1.66675C8.90573 1.66675 7.8221 1.8823 6.81105 2.30109C5.80001 2.71987 4.88135 3.3337 4.10753 4.10753C2.54472 5.67033 1.66675 7.78995 1.66675 10.0001C1.66675 12.2102 2.54472 14.3298 4.10753 15.8926C4.88135 16.6665 5.80001 17.2803 6.81105 17.6991C7.8221 18.1179 8.90573 18.3334 10.0001 18.3334C12.2102 18.3334 14.3298 17.4554 15.8926 15.8926C17.4554 14.3298 18.3334 12.2102 18.3334 10.0001C18.3334 5.39175 14.5834 1.66675 10.0001 1.66675Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
4
app/assets/icons/ic-arrow-left.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M16.6668 9.16663V10.8333L6.66677 10.8333L10.6584 14.825C10.9852 15.1517 10.9852 15.6815 10.6584 16.0083C10.3317 16.3351 9.80187 16.3351 9.4751 16.0083L3.46677 9.99996L9.4751 3.99163C9.80187 3.66486 10.3317 3.66486 10.6584 3.99163C10.9852 4.3184 10.9852 4.84819 10.6584 5.17496L6.66677 9.16663L16.6668 9.16663Z" />
|
||||
</svg>
|
||||
|
||||
|
After Width: | Height: | Size: 407 B |
3
app/assets/icons/ic-authenticator.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8 14C8 14.2652 8.10536 14.5196 8.29289 14.7071C8.48043 14.8946 8.73478 15 9 15C9.26522 15 9.51957 14.8946 9.70711 14.7071C9.89464 14.5196 10 14.2652 10 14C10 13.7348 9.89464 13.4804 9.70711 13.2929C9.51957 13.1054 9.26522 13 9 13C8.73478 13 8.48043 13.1054 8.29289 13.2929C8.10536 13.4804 8 13.7348 8 14ZM8 0V4H10V2.08C13.39 2.57 16 5.47 16 9C16 10.8565 15.2625 12.637 13.9497 13.9497C12.637 15.2625 10.8565 16 9 16C7.14348 16 5.36301 15.2625 4.05025 13.9497C2.7375 12.637 2 10.8565 2 9C2 7.32 2.59 5.78 3.58 4.58L9 10L10.41 8.59L3.61 1.79V1.81C1.42 3.45 0 6.05 0 9C0 11.3869 0.948211 13.6761 2.63604 15.364C4.32387 17.0518 6.61305 18 9 18C11.3869 18 13.6761 17.0518 15.364 15.364C17.0518 13.6761 18 11.3869 18 9C18 6.61305 17.0518 4.32387 15.364 2.63604C13.6761 0.948211 11.3869 0 9 0H8ZM15 9C15 8.73478 14.8946 8.48043 14.7071 8.29289C14.5196 8.10536 14.2652 8 14 8C13.7348 8 13.4804 8.10536 13.2929 8.29289C13.1054 8.48043 13 8.73478 13 9C13 9.26522 13.1054 9.51957 13.2929 9.70711C13.4804 9.89464 13.7348 10 14 10C14.2652 10 14.5196 9.89464 14.7071 9.70711C14.8946 9.51957 15 9.26522 15 9ZM3 9C3 9.26522 3.10536 9.51957 3.29289 9.70711C3.48043 9.89464 3.73478 10 4 10C4.26522 10 4.51957 9.89464 4.70711 9.70711C4.89464 9.51957 5 9.26522 5 9C5 8.73478 4.89464 8.48043 4.70711 8.29289C4.51957 8.10536 4.26522 8 4 8C3.73478 8 3.48043 8.10536 3.29289 8.29289C3.10536 8.48043 3 8.73478 3 9Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
3
app/assets/icons/ic-check-bold.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7.49995 17.0167L2.32495 11.8417L4.68328 9.48332L7.49995 12.3083L15.7333 4.06665L18.0916 6.42498L7.49995 17.0167Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 207 B |
4
app/assets/icons/ic-check-circle.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10 1.66666C5.41669 1.66666 1.66669 5.41666 1.66669 9.99999C1.66669 14.5833 5.41669 18.3333 10 18.3333C14.5834 18.3333 18.3334 14.5833 18.3334 9.99999C18.3334 5.41666 14.5834 1.66666 10 1.66666ZM10 16.6667C6.32502 16.6667 3.33335 13.675 3.33335 9.99999C3.33335 6.32499 6.32502 3.33332 10 3.33332C13.675 3.33332 16.6667 6.32499 16.6667 9.99999C16.6667 13.675 13.675 16.6667 10 16.6667ZM13.825 6.31666L8.33335 11.8083L6.17502 9.65832L5.00002 10.8333L8.33335 14.1667L15 7.49999L13.825 6.31666Z" />
|
||||
</svg>
|
||||
|
||||
|
After Width: | Height: | Size: 588 B |
3
app/assets/icons/ic-check.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M17.5001 5.83345L7.50008 15.8334L2.91675 11.2501L4.09175 10.0751L7.50008 13.4751L16.3251 4.65845L17.5001 5.83345Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 206 B |
4
app/assets/icons/ic-chevron-down.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6.17622 7.15015L10.0012 10.9751L13.8262 7.15015L15.0012 8.33348L10.0012 13.3335L5.00122 8.33348L6.17622 7.15015Z" fill="#72767E"/>
|
||||
</svg>
|
||||
|
||||
|
After Width: | Height: | Size: 225 B |
4
app/assets/icons/ic-cloud-off.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6.44167 8.33334L13.1083 15H5C4.11594 15 3.2681 14.6488 2.64298 14.0237C2.01786 13.3986 1.66667 12.5507 1.66667 11.6667C1.66667 10.7826 2.01786 9.93478 2.64298 9.30965C3.2681 8.68453 4.11594 8.33334 5 8.33334H6.44167ZM2.5 4.39168L4.79167 6.66668C2.13333 6.79168 0 8.97501 0 11.6667C0 12.9928 0.526784 14.2645 1.46447 15.2022C2.40215 16.1399 3.67392 16.6667 5 16.6667H14.775L16.4417 18.3333L17.5 17.275L3.55833 3.33334L2.5 4.39168ZM16.125 8.35834C15.5583 5.49168 13.0333 3.33334 10 3.33334C8.75 3.33334 7.625 3.69168 6.66667 4.30834L7.875 5.52501C8.50833 5.19168 9.23333 5.00001 10 5.00001C11.2156 5.00001 12.3814 5.4829 13.2409 6.34244C14.1004 7.20198 14.5833 8.36777 14.5833 9.58334V10H15.8333C16.4964 10 17.1323 10.2634 17.6011 10.7322C18.0699 11.2011 18.3333 11.837 18.3333 12.5C18.3333 13.4417 17.8 14.2583 17.0333 14.6833L18.2417 15.8917C19.3 15.1333 20 13.9 20 12.5C20 10.3 18.2917 8.51668 16.125 8.35834Z" />
|
||||
</svg>
|
||||
|
||||
|
After Width: | Height: | Size: 1009 B |
3
app/assets/icons/ic-code.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg viewBox="0 0 22 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11.89 0L13.85 0.4L10.11 18L8.15002 17.6L11.89 0ZM18.59 9L15 5.41V2.58L21.42 9L15 15.41V12.58L18.59 9ZM0.580017 9L7.00002 2.58V5.41L3.41002 9L7.00002 12.58V15.41L0.580017 9Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 267 B |
@@ -1,3 +1,3 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1.66724 3.66626C1.66724 2.56169 2.56267 1.66626 3.66724 1.66626H11.3339C12.4385 1.66626 13.3339 2.56169 13.3339 3.66626V13.3329H3.66724C2.56267 13.3329 1.66724 12.4375 1.66724 11.3329V3.66626ZM16.3339 6.66626C17.4385 6.66626 18.3339 7.56169 18.3339 8.66626V16.3329C18.3339 17.4375 17.4385 18.3329 16.3339 18.3329H8.66724C7.56267 18.3329 6.66724 17.4375 6.66724 16.3329V14.9996H15.0006V6.66626H16.3339ZM3.3339 3.33293V11.6663H11.6672V3.33293H3.3339Z" fill="#72767E"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 580 B After Width: | Height: | Size: 557 B |
@@ -1,3 +1,3 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11 3.5V9.5H12.17L10 11.67L7.83 9.5H9V3.5H11ZM13 1.5H7V7.5H3L10 14.5L17 7.5H13V1.5ZM17 16.5H3V18.5H17V16.5Z" fill="#72767E"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 238 B After Width: | Height: | Size: 215 B |
4
app/assets/icons/ic-email.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3.33317 3.33331H16.6665C17.1085 3.33331 17.5325 3.50891 17.845 3.82147C18.1576 4.13403 18.3332 4.55795 18.3332 4.99998V15C18.3332 15.442 18.1576 15.8659 17.845 16.1785C17.5325 16.491 17.1085 16.6666 16.6665 16.6666H3.33317C2.40817 16.6666 1.6665 15.9166 1.6665 15V4.99998C1.6665 4.07498 2.40817 3.33331 3.33317 3.33331ZM9.99984 9.16665L16.6665 4.99998H3.33317L9.99984 9.16665ZM3.33317 15H16.6665V6.97498L9.99984 11.1333L3.33317 6.97498V15Z" />
|
||||
</svg>
|
||||
|
||||
|
After Width: | Height: | Size: 538 B |
4
app/assets/icons/ic-eye-off.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1.66659 4.39159L2.73325 3.33325L16.6666 17.2666L15.6082 18.3333L13.0416 15.7666C12.0833 16.0833 11.0666 16.2499 9.99992 16.2499C5.83325 16.2499 2.27492 13.6583 0.833252 9.99992C1.40825 8.53325 2.32492 7.24159 3.49158 6.21659L1.66659 4.39159ZM9.99992 7.49992C10.663 7.49992 11.2988 7.76331 11.7677 8.23215C12.2365 8.70099 12.4999 9.33688 12.4999 9.99992C12.4999 10.2916 12.4499 10.5749 12.3582 10.8333L9.16658 7.64159C9.42492 7.54992 9.70825 7.49992 9.99992 7.49992ZM9.99992 3.74992C14.1666 3.74992 17.7249 6.34159 19.1666 9.99992C18.4832 11.7333 17.3249 13.2333 15.8332 14.3249L14.6499 13.1333C15.7832 12.3499 16.7166 11.2833 17.3499 9.99992C15.9749 7.19992 13.1333 5.41659 9.99992 5.41659C9.09158 5.41659 8.19992 5.56659 7.36658 5.83325L6.08325 4.55825C7.28325 4.04159 8.60825 3.74992 9.99992 3.74992ZM2.64992 9.99992C4.02492 12.7999 6.86658 14.5833 9.99992 14.5833C10.5749 14.5833 11.1416 14.5249 11.6666 14.4083L9.76658 12.4999C8.57492 12.3749 7.62492 11.4249 7.49992 10.2333L4.66658 7.39158C3.84158 8.09992 3.14992 8.98325 2.64992 9.99992Z" />
|
||||
</svg>
|
||||
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
4
app/assets/icons/ic-eye.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg viewBox="0 0 16 10" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7.99984 3C8.53027 3 9.03898 3.21071 9.41405 3.58579C9.78912 3.96086 9.99984 4.46957 9.99984 5C9.99984 5.53043 9.78912 6.03914 9.41405 6.41421C9.03898 6.78929 8.53027 7 7.99984 7C7.4694 7 6.9607 6.78929 6.58562 6.41421C6.21055 6.03914 5.99984 5.53043 5.99984 5C5.99984 4.46957 6.21055 3.96086 6.58562 3.58579C6.9607 3.21071 7.4694 3 7.99984 3ZM7.99984 0C11.3332 0 14.1798 2.07333 15.3332 5C14.1798 7.92667 11.3332 10 7.99984 10C4.6665 10 1.81984 7.92667 0.666504 5C1.81984 2.07333 4.6665 0 7.99984 0ZM2.11984 5C3.21984 7.24 5.49317 8.66667 7.99984 8.66667C10.5065 8.66667 12.7798 7.24 13.8798 5C12.7798 2.76 10.5065 1.33333 7.99984 1.33333C5.49317 1.33333 3.21984 2.76 2.11984 5Z" />
|
||||
</svg>
|
||||
|
||||
|
After Width: | Height: | Size: 777 B |
@@ -1,3 +1,3 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9.16675 15.0001H10.8334V13.3334H9.16675V15.0001ZM10.0001 1.66675C8.90573 1.66675 7.8221 1.8823 6.81105 2.30109C5.80001 2.71987 4.88135 3.3337 4.10753 4.10753C2.54472 5.67033 1.66675 7.78995 1.66675 10.0001C1.66675 12.2102 2.54472 14.3298 4.10753 15.8926C4.88135 16.6665 5.80001 17.2803 6.81105 17.6991C7.8221 18.1179 8.90573 18.3334 10.0001 18.3334C12.2102 18.3334 14.3298 17.4554 15.8926 15.8926C17.4554 14.3298 18.3334 12.2102 18.3334 10.0001C18.3334 8.90573 18.1179 7.8221 17.6991 6.81105C17.2803 5.80001 16.6665 4.88135 15.8926 4.10753C15.1188 3.3337 14.2002 2.71987 13.1891 2.30109C12.1781 1.8823 11.0944 1.66675 10.0001 1.66675ZM10.0001 16.6668C6.32508 16.6668 3.33342 13.6751 3.33342 10.0001C3.33342 6.32508 6.32508 3.33342 10.0001 3.33342C13.6751 3.33342 16.6668 6.32508 16.6668 10.0001C16.6668 13.6751 13.6751 16.6668 10.0001 16.6668ZM10.0001 5.00008C9.11603 5.00008 8.26818 5.35127 7.64306 5.97639C7.01794 6.60151 6.66675 7.44936 6.66675 8.33342H8.33342C8.33342 7.89139 8.50901 7.46747 8.82157 7.1549C9.13413 6.84234 9.55806 6.66675 10.0001 6.66675C10.4421 6.66675 10.866 6.84234 11.1786 7.1549C11.4912 7.46747 11.6667 7.89139 11.6667 8.33342C11.6667 10.0001 9.16675 9.79175 9.16675 12.5001H10.8334C10.8334 10.6251 13.3334 10.4167 13.3334 8.33342C13.3334 7.44936 12.9822 6.60151 12.3571 5.97639C11.732 5.35127 10.8841 5.00008 10.0001 5.00008Z" fill="#72767E"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
@@ -1,3 +1,3 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9.16675 7.50008H10.8334V5.83342H9.16675V7.50008ZM10.0001 16.6667C6.32508 16.6667 3.33341 13.6751 3.33341 10.0001C3.33341 6.32508 6.32508 3.33341 10.0001 3.33341C13.6751 3.33341 16.6667 6.32508 16.6667 10.0001C16.6667 13.6751 13.6751 16.6667 10.0001 16.6667ZM10.0001 1.66675C8.90573 1.66675 7.8221 1.8823 6.81105 2.30109C5.80001 2.71987 4.88135 3.3337 4.10752 4.10752C2.54472 5.67033 1.66675 7.78994 1.66675 10.0001C1.66675 12.2102 2.54472 14.3298 4.10752 15.8926C4.88135 16.6665 5.80001 17.2803 6.81105 17.6991C7.8221 18.1179 8.90573 18.3334 10.0001 18.3334C12.2102 18.3334 14.3298 17.4554 15.8926 15.8926C17.4554 14.3298 18.3334 12.2102 18.3334 10.0001C18.3334 8.90573 18.1179 7.8221 17.6991 6.81105C17.2803 5.80001 16.6665 4.88135 15.8926 4.10752C15.1188 3.3337 14.2002 2.71987 13.1891 2.30109C12.1781 1.8823 11.0944 1.66675 10.0001 1.66675ZM9.16675 14.1667H10.8334V9.16675H9.16675V14.1667Z" fill="#72767E"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1001 B |
@@ -1,3 +1,3 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3.33341 4.16675C2.89139 4.16675 2.46746 4.34234 2.1549 4.6549C1.84234 4.96746 1.66675 5.39139 1.66675 5.83341V14.1667C1.66675 14.6088 1.84234 15.0327 2.1549 15.3453C2.46746 15.6578 2.89139 15.8334 3.33341 15.8334H16.6667C17.1088 15.8334 17.5327 15.6578 17.8453 15.3453C18.1578 15.0327 18.3334 14.6088 18.3334 14.1667V5.83341C18.3334 5.39139 18.1578 4.96746 17.8453 4.6549C17.5327 4.34234 17.1088 4.16675 16.6667 4.16675H3.33341ZM3.33341 5.83341H16.6667V14.1667H3.33341V5.83341ZM4.16675 6.66675V8.33342H5.83341V6.66675H4.16675ZM6.66675 6.66675V8.33342H8.33341V6.66675H6.66675ZM9.16675 6.66675V8.33342H10.8334V6.66675H9.16675ZM11.6667 6.66675V8.33342H13.3334V6.66675H11.6667ZM14.1667 6.66675V8.33342H15.8334V6.66675H14.1667ZM4.16675 9.16675V10.8334H5.83341V9.16675H4.16675ZM6.66675 9.16675V10.8334H8.33341V9.16675H6.66675ZM9.16675 9.16675V10.8334H10.8334V9.16675H9.16675ZM11.6667 9.16675V10.8334H13.3334V9.16675H11.6667ZM14.1667 9.16675V10.8334H15.8334V9.16675H14.1667ZM6.66675 11.6667V13.3334H13.3334V11.6667H6.66675Z" fill="#72767E"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
@@ -1,3 +1,3 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.28571 3.6H15.7143C16.093 3.6 16.4 3.907 16.4 4.28571V15.7143C16.4 16.093 16.093 16.4 15.7143 16.4H4.28571C3.907 16.4 3.6 16.093 3.6 15.7143V4.28571C3.6 3.907 3.907 3.6 4.28571 3.6ZM2 4.28571C2 3.02335 3.02335 2 4.28571 2H15.7143C16.9767 2 18 3.02335 18 4.28571V15.7143C18 16.9767 16.9767 18 15.7143 18H4.28571C3.02335 18 2 16.9767 2 15.7143V4.28571ZM9.8045 6.31638C9.94745 6.27119 10.1703 6.24105 10.473 6.22599V6C10.0105 6.0226 9.35045 6.0339 8.49279 6.0339C7.59309 6.0339 6.92883 6.0226 6.5 6V6.22599C6.77748 6.24105 6.97928 6.27119 7.10541 6.31638C7.23994 6.36158 7.32823 6.44821 7.37027 6.57627C7.42072 6.70433 7.44595 6.91149 7.44595 7.19774V12.8023C7.44595 13.0885 7.42072 13.2957 7.37027 13.4237C7.32823 13.5518 7.23994 13.6384 7.10541 13.6836C6.97928 13.7288 6.77748 13.7589 6.5 13.774V14C7.40811 13.9774 8.77868 13.9661 10.6117 13.9661C11.9655 13.9661 12.9282 13.9774 13.5 14C13.4411 13.533 13.4117 12.9379 13.4117 12.2147C13.4117 11.8079 13.4243 11.4765 13.4495 11.2203H13.1595C13.0333 11.9812 12.7601 12.5913 12.3396 13.0508C11.9276 13.5104 11.4357 13.7401 10.864 13.7401H10.2459C10.0105 13.7401 9.83814 13.7213 9.72883 13.6836C9.62793 13.646 9.55646 13.5744 9.51441 13.4689C9.47237 13.3559 9.45135 13.1789 9.45135 12.9379V7.19774C9.45135 6.91149 9.47658 6.70433 9.52703 6.57627C9.57748 6.44821 9.66997 6.36158 9.8045 6.31638Z" fill="#72767E"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
4
app/assets/icons/ic-lock.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10.0002 14.1666C9.07516 14.1666 8.3335 13.4166 8.3335 12.5C8.3335 11.575 9.07516 10.8333 10.0002 10.8333C10.4422 10.8333 10.8661 11.0089 11.1787 11.3215C11.4912 11.634 11.6668 12.058 11.6668 12.5C11.6668 12.942 11.4912 13.3659 11.1787 13.6785C10.8661 13.9911 10.4422 14.1666 10.0002 14.1666ZM15.0002 16.6666V8.33331H5.00016V16.6666H15.0002ZM15.0002 6.66665C15.4422 6.66665 15.8661 6.84224 16.1787 7.1548C16.4912 7.46736 16.6668 7.89129 16.6668 8.33331V16.6666C16.6668 17.1087 16.4912 17.5326 16.1787 17.8452C15.8661 18.1577 15.4422 18.3333 15.0002 18.3333H5.00016C4.07516 18.3333 3.3335 17.5833 3.3335 16.6666V8.33331C3.3335 7.40831 4.07516 6.66665 5.00016 6.66665H5.8335V4.99998C5.8335 3.89491 6.27248 2.8351 7.05388 2.0537C7.83529 1.2723 8.89509 0.833313 10.0002 0.833313C10.5473 0.833313 11.0892 0.941087 11.5947 1.15048C12.1002 1.35988 12.5595 1.66679 12.9464 2.0537C13.3333 2.44061 13.6403 2.89994 13.8497 3.40547C14.0591 3.91099 14.1668 4.45281 14.1668 4.99998V6.66665H15.0002ZM10.0002 2.49998C9.33712 2.49998 8.70124 2.76337 8.23239 3.23221C7.76355 3.70105 7.50016 4.33694 7.50016 4.99998V6.66665H12.5002V4.99998C12.5002 4.33694 12.2368 3.70105 11.7679 3.23221C11.2991 2.76337 10.6632 2.49998 10.0002 2.49998Z" />
|
||||
</svg>
|
||||
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
3
app/assets/icons/ic-markdown.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11 0V2H14V14H11V16H16V0H11ZM0 0V16H5V14H2V2H5V0H0Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 144 B |
4
app/assets/icons/ic-menu-arrow-down.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg viewBox="0 0 8 4" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0.666656 0.666504L3.99999 3.99984L7.33332 0.666504H0.666656Z" />
|
||||
</svg>
|
||||
|
||||
|
After Width: | Height: | Size: 157 B |
@@ -1,3 +1,3 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10 10H15.4444C15.0322 12.9891 12.8933 15.6582 10 16.4873V10H4.55556V5.85455L10 3.59273V10ZM10 2L3 4.90909V9.27273C3 13.3091 5.98667 17.0764 10 18C14.0133 17.0764 17 13.3091 17 9.27273V4.90909L10 2Z" fill="#72767E"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 329 B After Width: | Height: | Size: 306 B |
4
app/assets/icons/ic-server.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10.8333 15.8333H11.6666C11.8876 15.8333 12.0996 15.9211 12.2559 16.0774C12.4122 16.2337 12.5 16.4457 12.5 16.6667H18.3333V18.3333H12.5C12.5 18.5543 12.4122 18.7663 12.2559 18.9226C12.0996 19.0789 11.8876 19.1667 11.6666 19.1667H8.33329C8.11228 19.1667 7.90032 19.0789 7.74404 18.9226C7.58776 18.7663 7.49996 18.5543 7.49996 18.3333H1.66663V16.6667H7.49996C7.49996 16.4457 7.58776 16.2337 7.74404 16.0774C7.90032 15.9211 8.11228 15.8333 8.33329 15.8333H9.16663V14.1667H3.33329C3.11228 14.1667 2.90032 14.0789 2.74404 13.9226C2.58776 13.7663 2.49996 13.5543 2.49996 13.3333V10C2.49996 9.77899 2.58776 9.56702 2.74404 9.41074C2.90032 9.25446 3.11228 9.16667 3.33329 9.16667H16.6666C16.8876 9.16667 17.0996 9.25446 17.2559 9.41074C17.4122 9.56702 17.5 9.77899 17.5 10V13.3333C17.5 13.5543 17.4122 13.7663 17.2559 13.9226C17.0996 14.0789 16.8876 14.1667 16.6666 14.1667H10.8333V15.8333ZM3.33329 2.5H16.6666C16.8876 2.5 17.0996 2.5878 17.2559 2.74408C17.4122 2.90036 17.5 3.11232 17.5 3.33333V6.66667C17.5 6.88768 17.4122 7.09964 17.2559 7.25592C17.0996 7.4122 16.8876 7.5 16.6666 7.5H3.33329C3.11228 7.5 2.90032 7.4122 2.74404 7.25592C2.58776 7.09964 2.49996 6.88768 2.49996 6.66667V3.33333C2.49996 3.11232 2.58776 2.90036 2.74404 2.74408C2.90032 2.5878 3.11228 2.5 3.33329 2.5ZM7.49996 5.83333H8.33329V4.16667H7.49996V5.83333ZM7.49996 12.5H8.33329V10.8333H7.49996V12.5ZM4.16663 4.16667V5.83333H5.83329V4.16667H4.16663ZM4.16663 10.8333V12.5H5.83329V10.8333H4.16663Z" />
|
||||
</svg>
|
||||
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -1,3 +1,3 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10.0001 6.66675C10.8842 6.66675 11.732 7.01794 12.3571 7.64306C12.9823 8.26818 13.3334 9.11603 13.3334 10.0001C13.3334 10.8841 12.9823 11.732 12.3571 12.3571C11.732 12.9822 10.8842 13.3334 10.0001 13.3334C9.11606 13.3334 8.26821 12.9822 7.64309 12.3571C7.01797 11.732 6.66678 10.8841 6.66678 10.0001C6.66678 9.11603 7.01797 8.26818 7.64309 7.64306C8.26821 7.01794 9.11606 6.66675 10.0001 6.66675ZM10.0001 8.33342C9.55808 8.33342 9.13416 8.50901 8.8216 8.82157C8.50904 9.13413 8.33344 9.55805 8.33344 10.0001C8.33344 10.4421 8.50904 10.866 8.8216 11.1786C9.13416 11.4912 9.55808 11.6667 10.0001 11.6667C10.4421 11.6667 10.8661 11.4912 11.1786 11.1786C11.4912 10.866 11.6668 10.4421 11.6668 10.0001C11.6668 9.55805 11.4912 9.13413 11.1786 8.82157C10.8661 8.50901 10.4421 8.33342 10.0001 8.33342ZM8.33344 18.3334C8.12511 18.3334 7.95011 18.1834 7.91678 17.9834L7.60844 15.7751C7.08344 15.5667 6.63344 15.2834 6.20011 14.9501L4.12511 15.7917C3.94178 15.8584 3.71678 15.7917 3.61678 15.6084L1.95011 12.7251C1.84178 12.5417 1.89178 12.3167 2.05011 12.1917L3.80844 10.8084L3.75011 10.0001L3.80844 9.16675L2.05011 7.80841C1.89178 7.68341 1.84178 7.45841 1.95011 7.27508L3.61678 4.39175C3.71678 4.20841 3.94178 4.13341 4.12511 4.20842L6.20011 5.04175C6.63344 4.71675 7.08344 4.43341 7.60844 4.22508L7.91678 2.01675C7.95011 1.81675 8.12511 1.66675 8.33344 1.66675H11.6668C11.8751 1.66675 12.0501 1.81675 12.0834 2.01675L12.3918 4.22508C12.9168 4.43341 13.3668 4.71675 13.8001 5.04175L15.8751 4.20842C16.0584 4.13341 16.2834 4.20841 16.3834 4.39175L18.0501 7.27508C18.1584 7.45841 18.1084 7.68341 17.9501 7.80841L16.1918 9.16675L16.2501 10.0001L16.1918 10.8334L17.9501 12.1917C18.1084 12.3167 18.1584 12.5417 18.0501 12.7251L16.3834 15.6084C16.2834 15.7917 16.0584 15.8667 15.8751 15.7917L13.8001 14.9584C13.3668 15.2834 12.9168 15.5667 12.3918 15.7751L12.0834 17.9834C12.0501 18.1834 11.8751 18.3334 11.6668 18.3334H8.33344ZM9.37511 3.33341L9.06678 5.50841C8.06678 5.71675 7.18344 6.25008 6.54178 6.99175L4.53344 6.12508L3.90844 7.20841L5.66678 8.50008C5.33344 9.47508 5.33344 10.5334 5.66678 11.5001L3.90011 12.8001L4.52511 13.8834L6.55011 13.0167C7.19178 13.7501 8.06678 14.2834 9.05844 14.4834L9.36678 16.6667H10.6334L10.9418 14.4917C11.9334 14.2834 12.8084 13.7501 13.4501 13.0167L15.4751 13.8834L16.1001 12.8001L14.3334 11.5084C14.6668 10.5334 14.6668 9.47508 14.3334 8.50008L16.0918 7.20841L15.4668 6.12508L13.4584 6.99175C12.8168 6.25008 11.9334 5.71675 10.9334 5.51675L10.6251 3.33341H9.37511Z" fill="#72767E"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.5 KiB |
4
app/assets/icons/ic-signin.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M15.8333 2.5H4.16667C3.24167 2.5 2.5 3.24167 2.5 4.16667V7.5H4.16667V4.16667H15.8333V15.8333H4.16667V12.5H2.5V15.8333C2.5 16.2754 2.67559 16.6993 2.98816 17.0118C3.30072 17.3244 3.72464 17.5 4.16667 17.5H15.8333C16.2754 17.5 16.6993 17.3244 17.0118 17.0118C17.3244 16.6993 17.5 16.2754 17.5 15.8333V4.16667C17.5 3.24167 16.75 2.5 15.8333 2.5ZM8.4 12.9833L9.58333 14.1667L13.75 10L9.58333 5.83333L8.4 7.00833L10.5583 9.16667H2.5V10.8333H10.5583L8.4 12.9833Z" />
|
||||
</svg>
|
||||
|
||||
|
After Width: | Height: | Size: 554 B |
4
app/assets/icons/ic-signout.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11.7333 12.9917L13.8917 10.8333H5.83333V9.16667H13.8917L11.7333 7.00833L12.9167 5.83333L17.0833 10L12.9167 14.1667L11.7333 12.9917ZM15.8333 2.5C16.2754 2.5 16.6993 2.67559 17.0118 2.98816C17.3244 3.30072 17.5 3.72464 17.5 4.16667V8.05833L15.8333 6.39167V4.16667H4.16667V15.8333H15.8333V13.6083L17.5 11.9417V15.8333C17.5 16.2754 17.3244 16.6993 17.0118 17.0118C16.6993 17.3244 16.2754 17.5 15.8333 17.5H4.16667C3.24167 17.5 2.5 16.75 2.5 15.8333V4.16667C2.5 3.24167 3.24167 2.5 4.16667 2.5H15.8333Z" />
|
||||
</svg>
|
||||
|
||||
|
After Width: | Height: | Size: 596 B |
3
app/assets/icons/ic-spreadsheets.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1.8 0.600098H16.2C16.6774 0.600098 17.1352 0.786413 17.4728 1.11806C17.8104 1.4497 18 1.8995 18 2.36852V15.6317C18 16.1007 17.8104 16.5505 17.4728 16.8821C17.1352 17.2138 16.6774 17.4001 16.2 17.4001H1.8C1.32261 17.4001 0.864773 17.2138 0.527208 16.8821C0.189642 16.5505 0 16.1007 0 15.6317V2.36852C0 1.8995 0.189642 1.4497 0.527208 1.11806C0.864773 0.786413 1.32261 0.600098 1.8 0.600098ZM1.8 4.13694V6.78957H5.4V4.13694H1.8ZM7.2 4.13694V6.78957H10.8V4.13694H7.2ZM16.2 6.78957V4.13694H12.6V6.78957H16.2ZM1.8 8.55799V11.2106H5.4V8.55799H1.8ZM1.8 15.6317H5.4V12.979H1.8V15.6317ZM7.2 8.55799V11.2106H10.8V8.55799H7.2ZM7.2 15.6317H10.8V12.979H7.2V15.6317ZM16.2 15.6317V12.979H12.6V15.6317H16.2ZM16.2 8.55799H12.6V11.2106H16.2V8.55799Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 826 B |
@@ -1,3 +1,3 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10.1035 12.8875C10.0399 12.8491 9.96028 12.8491 9.89672 12.8875L7.27153 14.4724C7.12022 14.5637 6.93347 14.4283 6.9733 14.2561L7.66462 11.2674C7.68131 11.1952 7.65676 11.1197 7.60082 11.0712L5.283 9.06056C5.14938 8.94465 5.22096 8.72509 5.39722 8.7102L8.45479 8.45191C8.52877 8.44566 8.5932 8.39895 8.62214 8.33058L9.8159 5.51022C9.88478 5.3475 10.1154 5.3475 10.1843 5.51022L11.378 8.33058C11.407 8.39895 11.4714 8.44566 11.5454 8.45191L14.6029 8.7102C14.7792 8.72509 14.8508 8.94465 14.7172 9.06057L12.3993 11.0712C12.3434 11.1197 12.3189 11.1952 12.3355 11.2674L13.0269 14.2561C13.0667 14.4283 12.8799 14.5637 12.7286 14.4724L10.1035 12.8875ZM17.9751 8.01046C18.1089 7.89462 18.0374 7.67496 17.8611 7.66001L12.4619 7.20194C12.388 7.19567 12.3236 7.149 12.2947 7.08071L10.1842 2.10122C10.1153 1.93862 9.88486 1.93862 9.81594 2.10122L7.70548 7.08071C7.67653 7.149 7.61216 7.19567 7.53824 7.20194L2.13848 7.66006C1.96228 7.67501 1.89074 7.89448 2.02429 8.01039L6.11748 11.5628C6.17343 11.6114 6.19795 11.6869 6.18122 11.759L4.95694 17.0392C4.91701 17.2114 5.10377 17.347 5.25512 17.2556L9.89674 14.4541C9.96029 14.4158 10.0399 14.4158 10.1034 14.4541L14.7443 17.2552C14.8957 17.3466 15.0826 17.2108 15.0424 17.0385L13.8108 11.7593C13.794 11.687 13.8185 11.6113 13.8747 11.5627L17.9751 8.01046Z" fill="#72767E"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
4
app/assets/icons/ic-sync.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9.99992 15C8.67384 15 7.40207 14.4732 6.46438 13.5355C5.5267 12.5978 4.99992 11.3261 4.99992 9.99998C4.99992 9.16665 5.20825 8.35831 5.58325 7.66665L4.36659 6.44998C3.71659 7.47498 3.33325 8.69165 3.33325 9.99998C3.33325 11.7681 4.03563 13.4638 5.28587 14.714C6.53612 15.9643 8.23181 16.6666 9.99992 16.6666V19.1666L13.3333 15.8333L9.99992 12.5V15ZM9.99992 3.33331V0.833313L6.66658 4.16665L9.99992 7.49998V4.99998C11.326 4.99998 12.5978 5.52676 13.5355 6.46445C14.4731 7.40213 14.9999 8.6739 14.9999 9.99998C14.9999 10.8333 14.7916 11.6416 14.4166 12.3333L15.6333 13.55C16.2833 12.525 16.6666 11.3083 16.6666 9.99998C16.6666 8.23187 15.9642 6.53618 14.714 5.28593C13.4637 4.03569 11.768 3.33331 9.99992 3.33331Z" />
|
||||
</svg>
|
||||
|
||||
|
After Width: | Height: | Size: 810 B |
3
app/assets/icons/ic-tasks.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M16 16H2V2H12V0H2C0.89 0 0 0.89 0 2V16C0 16.5304 0.210714 17.0391 0.585786 17.4142C0.960859 17.7893 1.46957 18 2 18H16C16.5304 18 17.0391 17.7893 17.4142 17.4142C17.7893 17.0391 18 16.5304 18 16V8H16V16ZM4.91 7.08L3.5 8.5L8 13L18 3L16.59 1.58L8 10.17L4.91 7.08Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 355 B |
4
app/assets/icons/ic-text-paragraph.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg viewBox="0 0 18 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M17.4 0V2H0.599976V0H17.4ZM0.599976 12H8.99998V10H0.599976V12ZM0.599976 7H17.4V5H0.599976V7Z" />
|
||||
</svg>
|
||||
|
||||
|
After Width: | Height: | Size: 190 B |
@@ -1,3 +1,3 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10 18C8.94943 18 7.90914 17.7931 6.93853 17.391C5.96793 16.989 5.08601 16.3997 4.34315 15.6569C2.84285 14.1566 2 12.1217 2 10C2 7.87827 2.84285 5.84344 4.34315 4.34315C5.84344 2.84285 7.87827 2 10 2C14.4 2 18 5.2 18 9.2C18 10.473 17.4943 11.6939 16.5941 12.5941C15.6939 13.4943 14.473 14 13.2 14H11.76C11.52 14 11.36 14.16 11.36 14.4C11.36 14.48 11.44 14.56 11.44 14.64C11.76 15.04 11.92 15.52 11.92 16C12 17.12 11.12 18 10 18ZM10 3.6C8.30261 3.6 6.67475 4.27428 5.47452 5.47452C4.27428 6.67475 3.6 8.30261 3.6 10C3.6 11.6974 4.27428 13.3253 5.47452 14.5255C6.67475 15.7257 8.30261 16.4 10 16.4C10.24 16.4 10.4 16.24 10.4 16C10.4 15.84 10.32 15.76 10.32 15.68C10 15.28 9.84 14.88 9.84 14.4C9.84 13.28 10.72 12.4 11.84 12.4H13.2C14.0487 12.4 14.8626 12.0629 15.4627 11.4627C16.0629 10.8626 16.4 10.0487 16.4 9.2C16.4 6.08 13.52 3.6 10 3.6ZM5.6 8.4C6.24 8.4 6.8 8.96 6.8 9.6C6.8 10.24 6.24 10.8 5.6 10.8C4.96 10.8 4.4 10.24 4.4 9.6C4.4 8.96 4.96 8.4 5.6 8.4ZM8 5.2C8.64 5.2 9.2 5.76 9.2 6.4C9.2 7.04 8.64 7.6 8 7.6C7.36 7.6 6.8 7.04 6.8 6.4C6.8 5.76 7.36 5.2 8 5.2ZM12 5.2C12.64 5.2 13.2 5.76 13.2 6.4C13.2 7.04 12.64 7.6 12 7.6C11.36 7.6 10.8 7.04 10.8 6.4C10.8 5.76 11.36 5.2 12 5.2ZM14.4 8.4C15.04 8.4 15.6 8.96 15.6 9.6C15.6 10.24 15.04 10.8 14.4 10.8C13.76 10.8 13.2 10.24 13.2 9.6C13.2 8.96 13.76 8.4 14.4 8.4Z" fill="#72767E"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
@@ -1,3 +1,3 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10 3C10.9283 3 11.8185 3.36875 12.4749 4.02513C13.1313 4.6815 13.5 5.57174 13.5 6.5C13.5 7.42826 13.1313 8.3185 12.4749 8.97487C11.8185 9.63125 10.9283 10 10 10C9.07174 10 8.1815 9.63125 7.52513 8.97487C6.86875 8.3185 6.5 7.42826 6.5 6.5C6.5 5.57174 6.86875 4.6815 7.52513 4.02513C8.1815 3.36875 9.07174 3 10 3ZM10 4.75C9.53587 4.75 9.09075 4.93437 8.76256 5.26256C8.43437 5.59075 8.25 6.03587 8.25 6.5C8.25 6.96413 8.43437 7.40925 8.76256 7.73744C9.09075 8.06563 9.53587 8.25 10 8.25C10.4641 8.25 10.9092 8.06563 11.2374 7.73744C11.5656 7.40925 11.75 6.96413 11.75 6.5C11.75 6.03587 11.5656 5.59075 11.2374 5.26256C10.9092 4.93437 10.4641 4.75 10 4.75ZM10 10.875C12.3363 10.875 17 12.0387 17 14.375V17H3V14.375C3 12.0387 7.66375 10.875 10 10.875ZM10 12.5375C7.40125 12.5375 4.6625 13.815 4.6625 14.375V15.3375H15.3375V14.375C15.3375 13.815 12.5988 12.5375 10 12.5375Z" fill="#72767E"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1000 B After Width: | Height: | Size: 977 B |
1
app/assets/javascripts/@types/qrcode.react.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
declare module 'qrcode.react';
|
||||
@@ -1,8 +1,5 @@
|
||||
'use strict';
|
||||
|
||||
declare const __VERSION__: string;
|
||||
declare const __WEB__: boolean;
|
||||
|
||||
import { SNLog } from '@standardnotes/snjs';
|
||||
import angular from 'angular';
|
||||
import { configRoutes } from './routes';
|
||||
@@ -66,6 +63,7 @@ import { NotesOptionsPanelDirective } from './components/NotesOptionsPanel';
|
||||
import { IconDirective } from './components/Icon';
|
||||
import { NoteTagsContainerDirective } from './components/NoteTagsContainer';
|
||||
import { PreferencesDirective } from './preferences';
|
||||
import { AppVersion, IsWebPlatform } from '@/version';
|
||||
|
||||
function reloadHiddenFirefoxTab(): boolean {
|
||||
/**
|
||||
@@ -91,7 +89,8 @@ function reloadHiddenFirefoxTab(): boolean {
|
||||
const startApplication: StartApplication = async function startApplication(
|
||||
defaultSyncServerHost: string,
|
||||
bridge: Bridge,
|
||||
webSocketUrl: string,
|
||||
enableUnfinishedFeatures: boolean,
|
||||
webSocketUrl: string
|
||||
) {
|
||||
if (reloadHiddenFirefoxTab()) {
|
||||
return;
|
||||
@@ -109,6 +108,7 @@ const startApplication: StartApplication = async function startApplication(
|
||||
.constant('bridge', bridge)
|
||||
.constant('defaultSyncServerHost', defaultSyncServerHost)
|
||||
.constant('appVersion', bridge.appVersion)
|
||||
.constant('enableUnfinishedFeatures', enableUnfinishedFeatures)
|
||||
.constant('webSocketUrl', webSocketUrl);
|
||||
|
||||
// Controllers
|
||||
@@ -191,11 +191,12 @@ const startApplication: StartApplication = async function startApplication(
|
||||
});
|
||||
};
|
||||
|
||||
if (__WEB__) {
|
||||
if (IsWebPlatform) {
|
||||
startApplication(
|
||||
(window as any)._default_sync_server,
|
||||
new BrowserBridge(__VERSION__),
|
||||
(window as any)._websocket_url,
|
||||
(window as any)._default_sync_server as string,
|
||||
new BrowserBridge(AppVersion),
|
||||
(window as any)._enable_unfinished_features as boolean,
|
||||
(window as any)._websocket_url as string,
|
||||
);
|
||||
} else {
|
||||
(window as any).startApplication = startApplication;
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import { WebApplication } from '@/ui_models/application';
|
||||
import { AppState } from '@/ui_models/app_state';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { FunctionComponent } from 'preact';
|
||||
import { useState } from 'preact/hooks';
|
||||
import { Checkbox } from '../Checkbox';
|
||||
import { Icon } from '../Icon';
|
||||
import { InputWithIcon } from '../InputWithIcon';
|
||||
|
||||
type Props = {
|
||||
application: WebApplication;
|
||||
appState: AppState;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export const AdvancedOptions: FunctionComponent<Props> = observer(
|
||||
({ appState, application, disabled = false, children }) => {
|
||||
const { server, setServer, enableServerOption, setEnableServerOption } =
|
||||
appState.accountMenu;
|
||||
const [showAdvanced, setShowAdvanced] = useState(false);
|
||||
|
||||
const handleServerOptionChange = (e: Event) => {
|
||||
if (e.target instanceof HTMLInputElement) {
|
||||
setEnableServerOption(e.target.checked);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSyncServerChange = (e: Event) => {
|
||||
if (e.target instanceof HTMLInputElement) {
|
||||
setServer(e.target.value);
|
||||
application.setCustomHost(e.target.value);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleShowAdvanced = () => {
|
||||
setShowAdvanced(!showAdvanced);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
className="sn-dropdown-item font-bold"
|
||||
onClick={toggleShowAdvanced}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
Advanced options
|
||||
<Icon type="chevron-down" className="color-grey-1 ml-1" />
|
||||
</div>
|
||||
</button>
|
||||
{showAdvanced ? (
|
||||
<div className="px-3 my-2">
|
||||
{children}
|
||||
<Checkbox
|
||||
name="custom-sync-server"
|
||||
label="Custom sync server"
|
||||
checked={enableServerOption}
|
||||
onChange={handleServerOptionChange}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<InputWithIcon
|
||||
inputType="text"
|
||||
icon="server"
|
||||
placeholder="https://api.standardnotes.com"
|
||||
value={server}
|
||||
onChange={handleSyncServerChange}
|
||||
disabled={!enableServerOption && !disabled}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
STRING_ACCOUNT_MENU_UNCHECK_MERGE,
|
||||
STRING_GENERATING_LOGIN_KEYS,
|
||||
STRING_GENERATING_REGISTER_KEYS,
|
||||
STRING_NON_MATCHING_PASSWORDS
|
||||
STRING_NON_MATCHING_PASSWORDS,
|
||||
} from '@/strings';
|
||||
import { JSXInternal } from 'preact/src/jsx';
|
||||
import TargetedEvent = JSXInternal.TargetedEvent;
|
||||
@@ -17,13 +17,9 @@ import { AppState } from '@/ui_models/app_state';
|
||||
type Props = {
|
||||
application: WebApplication;
|
||||
appState: AppState;
|
||||
}
|
||||
|
||||
const Authentication = observer(({
|
||||
application,
|
||||
appState,
|
||||
}: Props) => {
|
||||
};
|
||||
|
||||
const Authentication = observer(({ application, appState }: Props) => {
|
||||
const [showAdvanced, setShowAdvanced] = useState(false);
|
||||
const [isAuthenticating, setIsAuthenticating] = useState(false);
|
||||
const [email, setEmail] = useState('');
|
||||
@@ -39,16 +35,14 @@ const Authentication = observer(({
|
||||
const {
|
||||
server,
|
||||
notesAndTagsCount,
|
||||
showLogin,
|
||||
showSignIn,
|
||||
showRegister,
|
||||
setShowLogin,
|
||||
setShowSignIn,
|
||||
setShowRegister,
|
||||
setServer,
|
||||
closeAccountMenu
|
||||
closeAccountMenu,
|
||||
} = appState.accountMenu;
|
||||
|
||||
const user = application.getUser();
|
||||
|
||||
useEffect(() => {
|
||||
if (isEmailFocused) {
|
||||
emailInputRef.current.focus();
|
||||
@@ -58,11 +52,11 @@ const Authentication = observer(({
|
||||
|
||||
// Reset password and confirmation fields when hiding the form
|
||||
useEffect(() => {
|
||||
if (!showLogin && !showRegister) {
|
||||
if (!showSignIn && !showRegister) {
|
||||
setPassword('');
|
||||
setPasswordConfirmation('');
|
||||
}
|
||||
}, [showLogin, showRegister]);
|
||||
}, [showSignIn, showRegister]);
|
||||
|
||||
const handleHostInputChange = (event: TargetedEvent<HTMLInputElement>) => {
|
||||
const { value } = event.target as HTMLInputElement;
|
||||
@@ -75,7 +69,7 @@ const Authentication = observer(({
|
||||
const passwordConfirmationInputRef = useRef<HTMLInputElement>();
|
||||
|
||||
const handleSignInClick = () => {
|
||||
setShowLogin(true);
|
||||
setShowSignIn(true);
|
||||
setIsEmailFocused(true);
|
||||
};
|
||||
|
||||
@@ -90,7 +84,7 @@ const Authentication = observer(({
|
||||
passwordConfirmationInputRef.current?.blur();
|
||||
};
|
||||
|
||||
const login = async () => {
|
||||
const signin = async () => {
|
||||
setStatus(STRING_GENERATING_LOGIN_KEYS);
|
||||
setIsAuthenticating(true);
|
||||
|
||||
@@ -105,13 +99,13 @@ const Authentication = observer(({
|
||||
if (!error) {
|
||||
setIsAuthenticating(false);
|
||||
setPassword('');
|
||||
setShowLogin(false);
|
||||
setShowSignIn(false);
|
||||
|
||||
closeAccountMenu();
|
||||
return;
|
||||
}
|
||||
|
||||
setShowLogin(true);
|
||||
setShowSignIn(true);
|
||||
setStatus(undefined);
|
||||
setPassword('');
|
||||
|
||||
@@ -150,10 +144,11 @@ const Authentication = observer(({
|
||||
}
|
||||
};
|
||||
|
||||
const handleAuthFormSubmit = (event:
|
||||
TargetedEvent<HTMLFormElement> |
|
||||
TargetedMouseEvent<HTMLButtonElement> |
|
||||
TargetedKeyboardEvent<HTMLButtonElement>
|
||||
const handleAuthFormSubmit = (
|
||||
event:
|
||||
| TargetedEvent<HTMLFormElement>
|
||||
| TargetedMouseEvent<HTMLButtonElement>
|
||||
| TargetedKeyboardEvent<HTMLButtonElement>
|
||||
) => {
|
||||
event.preventDefault();
|
||||
|
||||
@@ -163,8 +158,8 @@ const Authentication = observer(({
|
||||
|
||||
blurAuthFields();
|
||||
|
||||
if (showLogin) {
|
||||
login();
|
||||
if (showSignIn) {
|
||||
signin();
|
||||
} else {
|
||||
register();
|
||||
}
|
||||
@@ -186,19 +181,23 @@ const Authentication = observer(({
|
||||
setEmail(value);
|
||||
};
|
||||
|
||||
const handlePasswordConfirmationChange = (event: TargetedEvent<HTMLInputElement>) => {
|
||||
const handlePasswordConfirmationChange = (
|
||||
event: TargetedEvent<HTMLInputElement>
|
||||
) => {
|
||||
const { value } = event.target as HTMLInputElement;
|
||||
setPasswordConfirmation(value);
|
||||
};
|
||||
|
||||
const handleMergeLocalData = async (event: TargetedEvent<HTMLInputElement>) => {
|
||||
const handleMergeLocalData = async (
|
||||
event: TargetedEvent<HTMLInputElement>
|
||||
) => {
|
||||
const { checked } = event.target as HTMLInputElement;
|
||||
|
||||
setShouldMergeLocal(checked);
|
||||
if (!checked) {
|
||||
const confirmResult = await confirmDialog({
|
||||
text: STRING_ACCOUNT_MENU_UNCHECK_MERGE,
|
||||
confirmButtonStyle: 'danger'
|
||||
confirmButtonStyle: 'danger',
|
||||
});
|
||||
setShouldMergeLocal(!confirmResult);
|
||||
}
|
||||
@@ -206,10 +205,12 @@ const Authentication = observer(({
|
||||
|
||||
return (
|
||||
<>
|
||||
{!user && !showLogin && !showRegister && (
|
||||
{!application.hasAccount() && !showSignIn && !showRegister && (
|
||||
<div className="sk-panel-section sk-panel-hero">
|
||||
<div className="sk-panel-row">
|
||||
<div className="sk-h1">Sign in or register to enable sync and end-to-end encryption.</div>
|
||||
<div className="sk-h1">
|
||||
Sign in or register to enable sync and end-to-end encryption.
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex my-1">
|
||||
<button
|
||||
@@ -226,17 +227,21 @@ const Authentication = observer(({
|
||||
</button>
|
||||
</div>
|
||||
<div className="sk-panel-row sk-p">
|
||||
Standard Notes is free on every platform, and comes
|
||||
standard with sync and encryption.
|
||||
Standard Notes is free on every platform, and comes standard with
|
||||
sync and encryption.
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{(showLogin || showRegister) && (
|
||||
{(showSignIn || showRegister) && (
|
||||
<div className="sk-panel-section">
|
||||
<div className="sk-panel-section-title">
|
||||
{showLogin ? 'Sign In' : 'Register'}
|
||||
{showSignIn ? 'Sign In' : 'Register'}
|
||||
</div>
|
||||
<form className="sk-panel-form" onSubmit={handleAuthFormSubmit} noValidate>
|
||||
<form
|
||||
className="sk-panel-form"
|
||||
onSubmit={handleAuthFormSubmit}
|
||||
noValidate
|
||||
>
|
||||
<div className="sk-panel-section">
|
||||
<input
|
||||
className="sk-input contrast"
|
||||
@@ -261,26 +266,28 @@ const Authentication = observer(({
|
||||
onKeyDown={handleKeyPressKeyDown}
|
||||
ref={passwordInputRef}
|
||||
/>
|
||||
{showRegister &&
|
||||
<input
|
||||
className="sk-input contrast"
|
||||
name="password_conf"
|
||||
type="password"
|
||||
placeholder="Confirm Password"
|
||||
required
|
||||
onKeyPress={handleKeyPressKeyDown}
|
||||
onKeyDown={handleKeyPressKeyDown}
|
||||
value={passwordConfirmation}
|
||||
onChange={handlePasswordConfirmationChange}
|
||||
ref={passwordConfirmationInputRef}
|
||||
/>}
|
||||
{showRegister && (
|
||||
<input
|
||||
className="sk-input contrast"
|
||||
name="password_conf"
|
||||
type="password"
|
||||
placeholder="Confirm Password"
|
||||
required
|
||||
onKeyPress={handleKeyPressKeyDown}
|
||||
onKeyDown={handleKeyPressKeyDown}
|
||||
value={passwordConfirmation}
|
||||
onChange={handlePasswordConfirmationChange}
|
||||
ref={passwordConfirmationInputRef}
|
||||
/>
|
||||
)}
|
||||
<div className="sk-panel-row" />
|
||||
<button
|
||||
type="button"
|
||||
className="sk-a info font-bold text-left p-0 cursor-pointer hover:underline mr-1 ml-1"
|
||||
onClick={() => {
|
||||
setShowAdvanced(!showAdvanced);
|
||||
}}>
|
||||
}}
|
||||
>
|
||||
Advanced Options
|
||||
</button>
|
||||
</div>
|
||||
@@ -301,24 +308,28 @@ const Authentication = observer(({
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
{showLogin && (
|
||||
{showSignIn && (
|
||||
<label className="sk-label padded-row sk-panel-row justify-left">
|
||||
<div className="sk-horizontal-group tight cursor-pointer">
|
||||
<input
|
||||
className="sk-input"
|
||||
type="checkbox"
|
||||
checked={isStrictSignIn}
|
||||
onChange={() => setIsStrictSignIn(prevState => !prevState)}
|
||||
onChange={() =>
|
||||
setIsStrictSignIn((prevState) => !prevState)
|
||||
}
|
||||
/>
|
||||
<p className="sk-p">Use strict sign in</p>
|
||||
<span>
|
||||
<a className="info"
|
||||
href="https://standardnotes.com/help/security" rel="noopener"
|
||||
target="_blank"
|
||||
>
|
||||
(Learn more)
|
||||
</a>
|
||||
</span>
|
||||
<a
|
||||
className="info"
|
||||
href="https://standardnotes.com/help/security"
|
||||
rel="noopener"
|
||||
target="_blank"
|
||||
>
|
||||
(Learn more)
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
</label>
|
||||
)}
|
||||
@@ -327,9 +338,12 @@ const Authentication = observer(({
|
||||
)}
|
||||
{!isAuthenticating && (
|
||||
<div className="sk-panel-section form-submit">
|
||||
<button className="sn-button info text-base py-3 text-center" type="submit"
|
||||
disabled={isAuthenticating}>
|
||||
{showLogin ? 'Sign In' : 'Register'}
|
||||
<button
|
||||
className="sn-button info text-base py-3 text-center"
|
||||
type="submit"
|
||||
disabled={isAuthenticating}
|
||||
>
|
||||
{showSignIn ? 'Sign In' : 'Register'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
@@ -337,9 +351,9 @@ const Authentication = observer(({
|
||||
<div className="sk-notification neutral">
|
||||
<div className="sk-notification-title">No Password Reset.</div>
|
||||
<div className="sk-notification-text">
|
||||
Because your notes are encrypted using your password,
|
||||
Standard Notes does not have a password reset option.
|
||||
You cannot forget your password.
|
||||
Because your notes are encrypted using your password, Standard
|
||||
Notes does not have a password reset option. You cannot forget
|
||||
your password.
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -358,7 +372,7 @@ const Authentication = observer(({
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={!isEphemeral}
|
||||
onChange={() => setIsEphemeral(prevState => !prevState)}
|
||||
onChange={() => setIsEphemeral((prevState) => !prevState)}
|
||||
/>
|
||||
<p className="sk-p">Stay signed in</p>
|
||||
</div>
|
||||
@@ -371,7 +385,9 @@ const Authentication = observer(({
|
||||
checked={shouldMergeLocal}
|
||||
onChange={handleMergeLocalData}
|
||||
/>
|
||||
<p className="sk-p">Merge local data ({notesAndTagsCount}) notes and tags</p>
|
||||
<p className="sk-p">
|
||||
Merge local data ({notesAndTagsCount}) notes and tags
|
||||
</p>
|
||||
</div>
|
||||
</label>
|
||||
)}
|
||||
@@ -379,7 +395,8 @@ const Authentication = observer(({
|
||||
)}
|
||||
</form>
|
||||
</div>
|
||||
)}</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
import { STRING_NON_MATCHING_PASSWORDS } from '@/strings';
|
||||
import { WebApplication } from '@/ui_models/application';
|
||||
import { AppState } from '@/ui_models/app_state';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { FunctionComponent } from 'preact';
|
||||
import { StateUpdater, useRef, useState } from 'preact/hooks';
|
||||
import { AccountMenuPane } from '.';
|
||||
import { Button } from '../Button';
|
||||
import { Checkbox } from '../Checkbox';
|
||||
import { IconButton } from '../IconButton';
|
||||
import { InputWithIcon } from '../InputWithIcon';
|
||||
import { AdvancedOptions } from './AdvancedOptions';
|
||||
|
||||
type Props = {
|
||||
appState: AppState;
|
||||
application: WebApplication;
|
||||
setMenuPane: (pane: AccountMenuPane) => void;
|
||||
email: string;
|
||||
password: string;
|
||||
setPassword: StateUpdater<string>;
|
||||
};
|
||||
|
||||
export const ConfirmPassword: FunctionComponent<Props> = observer(
|
||||
({ application, appState, setMenuPane, email, password, setPassword }) => {
|
||||
const { notesAndTagsCount } = appState.accountMenu;
|
||||
const [confirmPassword, setConfirmPassword] = useState('');
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [isRegistering, setIsRegistering] = useState(false);
|
||||
const [isEphemeral, setIsEphemeral] = useState(false);
|
||||
const [shouldMergeLocal, setShouldMergeLocal] = useState(true);
|
||||
|
||||
const passwordInputRef = useRef<HTMLInputElement>();
|
||||
|
||||
const handlePasswordChange = (e: Event) => {
|
||||
if (e.target instanceof HTMLInputElement) {
|
||||
setConfirmPassword(e.target.value);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEphemeralChange = () => {
|
||||
setIsEphemeral(!isEphemeral);
|
||||
};
|
||||
|
||||
const handleShouldMergeChange = () => {
|
||||
setShouldMergeLocal(!shouldMergeLocal);
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Enter') {
|
||||
handleConfirmFormSubmit(e);
|
||||
}
|
||||
};
|
||||
|
||||
const handleConfirmFormSubmit = (e: Event) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!password) {
|
||||
passwordInputRef?.current.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (password === confirmPassword) {
|
||||
setIsRegistering(true);
|
||||
application
|
||||
.register(email, password, isEphemeral, shouldMergeLocal)
|
||||
.then((res) => {
|
||||
if (res.error) {
|
||||
throw new Error(res.error.message);
|
||||
}
|
||||
appState.accountMenu.closeAccountMenu();
|
||||
appState.accountMenu.setCurrentPane(AccountMenuPane.GeneralMenu);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
application.alertService.alert(err).finally(() => {
|
||||
setPassword('');
|
||||
handleGoBack();
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
setIsRegistering(false);
|
||||
});
|
||||
} else {
|
||||
application.alertService
|
||||
.alert(STRING_NON_MATCHING_PASSWORDS)
|
||||
.finally(() => {
|
||||
setConfirmPassword('');
|
||||
passwordInputRef?.current.focus();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleGoBack = () => {
|
||||
setMenuPane(AccountMenuPane.Register);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-center px-3 mt-1 mb-3">
|
||||
<IconButton
|
||||
icon="arrow-left"
|
||||
title="Go back"
|
||||
className="flex mr-2 color-neutral"
|
||||
onClick={handleGoBack}
|
||||
focusable={true}
|
||||
disabled={isRegistering}
|
||||
/>
|
||||
<div className="sn-account-menu-headline">Confirm password</div>
|
||||
</div>
|
||||
<div className="px-3 mb-3 text-sm">
|
||||
Because your notes are encrypted using your password,{' '}
|
||||
<span className="color-dark-red">
|
||||
Standard Notes does not have a password reset option
|
||||
</span>
|
||||
. If you forget your password, you will permanently lose access to
|
||||
your data.
|
||||
</div>
|
||||
<form onSubmit={handleConfirmFormSubmit} className="px-3 mb-1">
|
||||
<InputWithIcon
|
||||
className="mb-2"
|
||||
icon="password"
|
||||
inputType={showPassword ? 'text' : 'password'}
|
||||
placeholder="Confirm password"
|
||||
value={confirmPassword}
|
||||
onChange={handlePasswordChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
toggle={{
|
||||
toggleOnIcon: 'eye-off',
|
||||
toggleOffIcon: 'eye',
|
||||
title: 'Show password',
|
||||
toggled: showPassword,
|
||||
onClick: setShowPassword,
|
||||
}}
|
||||
ref={passwordInputRef}
|
||||
disabled={isRegistering}
|
||||
/>
|
||||
<Button
|
||||
className="btn-w-full mt-1 mb-3"
|
||||
label={
|
||||
isRegistering ? 'Creating account...' : 'Create account & sign in'
|
||||
}
|
||||
type="primary"
|
||||
onClick={handleConfirmFormSubmit}
|
||||
disabled={isRegistering}
|
||||
/>
|
||||
<Checkbox
|
||||
name="is-ephemeral"
|
||||
label="Stay signed in"
|
||||
checked={!isEphemeral}
|
||||
onChange={handleEphemeralChange}
|
||||
disabled={isRegistering}
|
||||
/>
|
||||
{notesAndTagsCount > 0 ? (
|
||||
<Checkbox
|
||||
name="should-merge-local"
|
||||
label={`Merge local data (${notesAndTagsCount} notes and tags)`}
|
||||
checked={shouldMergeLocal}
|
||||
onChange={handleShouldMergeChange}
|
||||
disabled={isRegistering}
|
||||
/>
|
||||
) : null}
|
||||
</form>
|
||||
<div className="h-1px my-2 bg-border"></div>
|
||||
<AdvancedOptions
|
||||
appState={appState}
|
||||
application={application}
|
||||
disabled={isRegistering}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
137
app/assets/javascripts/components/AccountMenu/CreateAccount.tsx
Normal file
@@ -0,0 +1,137 @@
|
||||
import { WebApplication } from '@/ui_models/application';
|
||||
import { AppState } from '@/ui_models/app_state';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { FunctionComponent } from 'preact';
|
||||
import { StateUpdater, useEffect, useRef, useState } from 'preact/hooks';
|
||||
import { AccountMenuPane } from '.';
|
||||
import { Button } from '../Button';
|
||||
import { IconButton } from '../IconButton';
|
||||
import { InputWithIcon } from '../InputWithIcon';
|
||||
import { AdvancedOptions } from './AdvancedOptions';
|
||||
|
||||
type Props = {
|
||||
appState: AppState;
|
||||
application: WebApplication;
|
||||
setMenuPane: (pane: AccountMenuPane) => void;
|
||||
email: string;
|
||||
setEmail: StateUpdater<string>;
|
||||
password: string;
|
||||
setPassword: StateUpdater<string>;
|
||||
};
|
||||
|
||||
export const CreateAccount: FunctionComponent<Props> = observer(
|
||||
({
|
||||
appState,
|
||||
application,
|
||||
setMenuPane,
|
||||
email,
|
||||
setEmail,
|
||||
password,
|
||||
setPassword,
|
||||
}) => {
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
|
||||
const emailInputRef = useRef<HTMLInputElement>();
|
||||
const passwordInputRef = useRef<HTMLInputElement>();
|
||||
|
||||
useEffect(() => {
|
||||
if (emailInputRef.current) {
|
||||
emailInputRef.current.focus();
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleEmailChange = (e: Event) => {
|
||||
if (e.target instanceof HTMLInputElement) {
|
||||
setEmail(e.target.value);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePasswordChange = (e: Event) => {
|
||||
if (e.target instanceof HTMLInputElement) {
|
||||
setPassword(e.target.value);
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Enter') {
|
||||
handleRegisterFormSubmit(e);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRegisterFormSubmit = (e: Event) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!email || email.length === 0) {
|
||||
emailInputRef?.current.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!password || password.length === 0) {
|
||||
passwordInputRef?.current.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
setEmail(email);
|
||||
setPassword(password);
|
||||
setMenuPane(AccountMenuPane.ConfirmPassword);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setMenuPane(AccountMenuPane.GeneralMenu);
|
||||
setEmail('');
|
||||
setPassword('');
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-center px-3 mt-1 mb-3">
|
||||
<IconButton
|
||||
icon="arrow-left"
|
||||
title="Go back"
|
||||
className="flex mr-2 color-neutral"
|
||||
onClick={handleClose}
|
||||
focusable={true}
|
||||
/>
|
||||
<div className="sn-account-menu-headline">Create account</div>
|
||||
</div>
|
||||
<form onSubmit={handleRegisterFormSubmit} className="px-3 mb-1">
|
||||
<InputWithIcon
|
||||
className="mb-2"
|
||||
icon="email"
|
||||
inputType="email"
|
||||
placeholder="Email"
|
||||
value={email}
|
||||
onChange={handleEmailChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
ref={emailInputRef}
|
||||
/>
|
||||
<InputWithIcon
|
||||
className="mb-2"
|
||||
icon="password"
|
||||
inputType={showPassword ? 'text' : 'password'}
|
||||
placeholder="Password"
|
||||
value={password}
|
||||
onChange={handlePasswordChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
toggle={{
|
||||
toggleOnIcon: 'eye-off',
|
||||
toggleOffIcon: 'eye',
|
||||
title: 'Show password',
|
||||
toggled: showPassword,
|
||||
onClick: setShowPassword,
|
||||
}}
|
||||
ref={passwordInputRef}
|
||||
/>
|
||||
<Button
|
||||
className="btn-w-full mt-1"
|
||||
label="Next"
|
||||
type="primary"
|
||||
onClick={handleRegisterFormSubmit}
|
||||
/>
|
||||
</form>
|
||||
<div className="h-1px my-2 bg-border"></div>
|
||||
<AdvancedOptions application={application} appState={appState} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
@@ -6,25 +6,24 @@ import { observer } from 'mobx-react-lite';
|
||||
type Props = {
|
||||
application: WebApplication;
|
||||
appState: AppState;
|
||||
}
|
||||
};
|
||||
|
||||
const Footer = observer(({
|
||||
application,
|
||||
appState,
|
||||
}: Props) => {
|
||||
const Footer = observer(({ application, appState }: Props) => {
|
||||
const {
|
||||
showLogin,
|
||||
showSignIn,
|
||||
showRegister,
|
||||
setShowLogin,
|
||||
setShowSignIn,
|
||||
setShowRegister,
|
||||
setSigningOut
|
||||
setSigningOut,
|
||||
} = appState.accountMenu;
|
||||
|
||||
const user = application.getUser();
|
||||
const { showBetaWarning, disableBetaWarning: disableAppStateBetaWarning } =
|
||||
appState;
|
||||
|
||||
const { showBetaWarning, disableBetaWarning: disableAppStateBetaWarning } = appState;
|
||||
|
||||
const [appVersion] = useState(() => `v${((window as any).electronAppVersion || application.bridge.appVersion)}`);
|
||||
const [appVersion] = useState(
|
||||
() =>
|
||||
`v${(window as any).electronAppVersion || application.bridge.appVersion}`
|
||||
);
|
||||
|
||||
const disableBetaWarning = () => {
|
||||
disableAppStateBetaWarning();
|
||||
@@ -35,7 +34,7 @@ const Footer = observer(({
|
||||
};
|
||||
|
||||
const hidePasswordForm = () => {
|
||||
setShowLogin(false);
|
||||
setShowSignIn(false);
|
||||
setShowRegister(false);
|
||||
};
|
||||
|
||||
@@ -46,18 +45,22 @@ const Footer = observer(({
|
||||
<span>{appVersion}</span>
|
||||
{showBetaWarning && (
|
||||
<span>
|
||||
<span> (</span>
|
||||
<a className="sk-a" onClick={disableBetaWarning}>Hide beta warning</a>
|
||||
<span>)</span>
|
||||
</span>
|
||||
<span> (</span>
|
||||
<a className="sk-a" onClick={disableBetaWarning}>
|
||||
Hide beta warning
|
||||
</a>
|
||||
<span>)</span>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{(showLogin || showRegister) && (
|
||||
<a className="sk-a right" onClick={hidePasswordForm}>Cancel</a>
|
||||
{(showSignIn || showRegister) && (
|
||||
<a className="sk-a right" onClick={hidePasswordForm}>
|
||||
Cancel
|
||||
</a>
|
||||
)}
|
||||
{!showLogin && !showRegister && (
|
||||
{!showSignIn && !showRegister && (
|
||||
<a className="sk-a right danger capitalize" onClick={signOut}>
|
||||
{user ? 'Sign out' : 'Clear session data'}
|
||||
{application.hasAccount() ? 'Sign out' : 'Clear session data'}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
import { WebApplication } from '@/ui_models/application';
|
||||
import { AppState } from '@/ui_models/app_state';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { Icon } from '../Icon';
|
||||
import { formatLastSyncDate } from '@/preferences/panes/account/Sync';
|
||||
import { SyncQueueStrategy } from '@standardnotes/snjs';
|
||||
import { STRING_GENERIC_SYNC_ERROR } from '@/strings';
|
||||
import { useState } from 'preact/hooks';
|
||||
import { AccountMenuPane } from '.';
|
||||
import { FunctionComponent } from 'preact';
|
||||
|
||||
type Props = {
|
||||
appState: AppState;
|
||||
application: WebApplication;
|
||||
setMenuPane: (pane: AccountMenuPane) => void;
|
||||
closeMenu: () => void;
|
||||
};
|
||||
|
||||
const iconClassName = 'color-grey-1 mr-2';
|
||||
|
||||
export const GeneralAccountMenu: FunctionComponent<Props> = observer(
|
||||
({ application, appState, setMenuPane, closeMenu }) => {
|
||||
const [isSyncingInProgress, setIsSyncingInProgress] = useState(false);
|
||||
const [lastSyncDate, setLastSyncDate] = useState(
|
||||
formatLastSyncDate(application.getLastSyncDate() as Date)
|
||||
);
|
||||
|
||||
const doSynchronization = async () => {
|
||||
setIsSyncingInProgress(true);
|
||||
|
||||
application
|
||||
.sync({
|
||||
queueStrategy: SyncQueueStrategy.ForceSpawnNew,
|
||||
checkIntegrity: true,
|
||||
})
|
||||
.then((res) => {
|
||||
if (res && res.error) {
|
||||
throw new Error();
|
||||
} else {
|
||||
setLastSyncDate(
|
||||
formatLastSyncDate(application.getLastSyncDate() as Date)
|
||||
);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
application.alertService.alert(STRING_GENERIC_SYNC_ERROR);
|
||||
})
|
||||
.finally(() => {
|
||||
setIsSyncingInProgress(false);
|
||||
});
|
||||
};
|
||||
|
||||
const user = application.getUser();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-center justify-between px-3 mt-1 mb-3">
|
||||
<div className="sn-account-menu-headline">Account</div>
|
||||
<div className="flex cursor-pointer" onClick={closeMenu}>
|
||||
<Icon type="close" className="color-grey-1" />
|
||||
</div>
|
||||
</div>
|
||||
{user ? (
|
||||
<>
|
||||
<div className="px-3 mb-2 color-foreground text-sm">
|
||||
<div>You're signed in as:</div>
|
||||
<div className="font-bold">{user.email}</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between px-3 mb-2">
|
||||
{isSyncingInProgress ? (
|
||||
<div className="flex items-center color-info font-semibold">
|
||||
<div className="sk-spinner w-5 h-5 mr-2 spinner-info"></div>
|
||||
Syncing...
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center success font-semibold">
|
||||
<Icon type="check-circle" className="mr-2" />
|
||||
Last synced: {lastSyncDate}
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className="flex cursor-pointer color-grey-1"
|
||||
onClick={doSynchronization}
|
||||
>
|
||||
<Icon type="sync" />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="px-3 mb-1">
|
||||
<div className="mb-3 color-foreground">
|
||||
You’re offline. Sign in to sync your notes and preferences
|
||||
across all your devices and enable end-to-end encryption.
|
||||
</div>
|
||||
<div className="flex items-center color-grey-1">
|
||||
<Icon type="cloud-off" className="mr-2" />
|
||||
<span className="font-semibold">Offline</span>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div className="h-1px my-2 bg-border"></div>
|
||||
{user ? (
|
||||
<button
|
||||
className="sn-dropdown-item"
|
||||
onClick={() => {
|
||||
appState.accountMenu.closeAccountMenu();
|
||||
appState.preferences.setCurrentPane('account');
|
||||
appState.preferences.openPreferences();
|
||||
}}
|
||||
>
|
||||
<Icon type="user" className={iconClassName} />
|
||||
Account settings
|
||||
</button>
|
||||
) : (
|
||||
<>
|
||||
<button
|
||||
className="sn-dropdown-item"
|
||||
onClick={() => {
|
||||
setMenuPane(AccountMenuPane.Register);
|
||||
}}
|
||||
>
|
||||
<Icon type="user" className={iconClassName} />
|
||||
Create free account
|
||||
</button>
|
||||
<button
|
||||
className="sn-dropdown-item"
|
||||
onClick={() => {
|
||||
setMenuPane(AccountMenuPane.SignIn);
|
||||
}}
|
||||
>
|
||||
<Icon type="signIn" className={iconClassName} />
|
||||
Sign in
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
<button
|
||||
className="sn-dropdown-item"
|
||||
onClick={() => {
|
||||
appState.accountMenu.closeAccountMenu();
|
||||
appState.preferences.setCurrentPane('help-feedback');
|
||||
appState.preferences.openPreferences();
|
||||
}}
|
||||
>
|
||||
<Icon type="help" className={iconClassName} />
|
||||
Help & feedback
|
||||
</button>
|
||||
{user ? (
|
||||
<>
|
||||
<div className="h-1px my-2 bg-border"></div>
|
||||
<button
|
||||
className="sn-dropdown-item"
|
||||
onClick={() => {
|
||||
appState.accountMenu.setSigningOut(true);
|
||||
}}
|
||||
>
|
||||
<Icon type="signOut" className={iconClassName} />
|
||||
Sign out and clear local data
|
||||
</button>
|
||||
</>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
@@ -40,7 +40,6 @@ const PasscodeLock = observer(({
|
||||
const [canAddPasscode, setCanAddPasscode] = useState(!application.isEphemeralSession());
|
||||
const [hasPasscode, setHasPasscode] = useState(application.hasPasscode());
|
||||
|
||||
|
||||
const handleAddPassCode = () => {
|
||||
setShowPasscodeForm(true);
|
||||
setIsPasscodeFocused(true);
|
||||
|
||||
227
app/assets/javascripts/components/AccountMenu/SignIn.tsx
Normal file
@@ -0,0 +1,227 @@
|
||||
import { WebApplication } from '@/ui_models/application';
|
||||
import { AppState } from '@/ui_models/app_state';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { FunctionComponent } from 'preact';
|
||||
import { useEffect, useRef, useState } from 'preact/hooks';
|
||||
import { AccountMenuPane } from '.';
|
||||
import { Button } from '../Button';
|
||||
import { Checkbox } from '../Checkbox';
|
||||
import { Icon } from '../Icon';
|
||||
import { IconButton } from '../IconButton';
|
||||
import { InputWithIcon } from '../InputWithIcon';
|
||||
import { AdvancedOptions } from './AdvancedOptions';
|
||||
|
||||
type Props = {
|
||||
appState: AppState;
|
||||
application: WebApplication;
|
||||
setMenuPane: (pane: AccountMenuPane) => void;
|
||||
};
|
||||
|
||||
export const SignInPane: FunctionComponent<Props> = observer(
|
||||
({ application, appState, setMenuPane }) => {
|
||||
const { notesAndTagsCount } = appState.accountMenu;
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [isInvalid, setIsInvalid] = useState(false);
|
||||
const [isEphemeral, setIsEphemeral] = useState(false);
|
||||
const [isStrictSignin, setIsStrictSignin] = useState(false);
|
||||
const [isSigningIn, setIsSigningIn] = useState(false);
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [shouldMergeLocal, setShouldMergeLocal] = useState(true);
|
||||
|
||||
const emailInputRef = useRef<HTMLInputElement>();
|
||||
const passwordInputRef = useRef<HTMLInputElement>();
|
||||
|
||||
useEffect(() => {
|
||||
if (emailInputRef?.current) {
|
||||
emailInputRef.current.focus();
|
||||
}
|
||||
}, []);
|
||||
|
||||
const resetInvalid = () => {
|
||||
if (isInvalid) {
|
||||
setIsInvalid(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEmailChange = (e: Event) => {
|
||||
if (e.target instanceof HTMLInputElement) {
|
||||
setEmail(e.target.value);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePasswordChange = (e: Event) => {
|
||||
if (isInvalid) {
|
||||
setIsInvalid(false);
|
||||
}
|
||||
if (e.target instanceof HTMLInputElement) {
|
||||
setPassword(e.target.value);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEphemeralChange = () => {
|
||||
setIsEphemeral(!isEphemeral);
|
||||
};
|
||||
|
||||
const handleStrictSigninChange = () => {
|
||||
setIsStrictSignin(!isStrictSignin);
|
||||
};
|
||||
|
||||
const handleShouldMergeChange = () => {
|
||||
setShouldMergeLocal(!shouldMergeLocal);
|
||||
};
|
||||
|
||||
const signIn = () => {
|
||||
setIsSigningIn(true);
|
||||
emailInputRef?.current.blur();
|
||||
passwordInputRef?.current.blur();
|
||||
|
||||
application
|
||||
.signIn(email, password, isStrictSignin, isEphemeral, shouldMergeLocal)
|
||||
.then((res) => {
|
||||
if (res.error) {
|
||||
throw new Error(res.error.message);
|
||||
}
|
||||
appState.accountMenu.closeAccountMenu();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
if (err.toString().includes('Invalid email or password')) {
|
||||
setIsInvalid(true);
|
||||
} else {
|
||||
application.alertService.alert(err);
|
||||
}
|
||||
setPassword('');
|
||||
passwordInputRef?.current.blur();
|
||||
})
|
||||
.finally(() => {
|
||||
setIsSigningIn(false);
|
||||
});
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Enter') {
|
||||
handleSignInFormSubmit(e);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSignInFormSubmit = (e: Event) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!email || email.length === 0) {
|
||||
emailInputRef?.current.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!password || password.length === 0) {
|
||||
passwordInputRef?.current.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
signIn();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-center px-3 mt-1 mb-3">
|
||||
<IconButton
|
||||
icon="arrow-left"
|
||||
title="Go back"
|
||||
className="flex mr-2 color-neutral"
|
||||
onClick={() => setMenuPane(AccountMenuPane.GeneralMenu)}
|
||||
focusable={true}
|
||||
disabled={isSigningIn}
|
||||
/>
|
||||
<div className="sn-account-menu-headline">Sign in</div>
|
||||
</div>
|
||||
<form onSubmit={handleSignInFormSubmit}>
|
||||
<div className="px-3 mb-1">
|
||||
<InputWithIcon
|
||||
className={`mb-2 ${isInvalid ? 'border-dark-red' : null}`}
|
||||
icon="email"
|
||||
inputType="email"
|
||||
placeholder="Email"
|
||||
value={email}
|
||||
onChange={handleEmailChange}
|
||||
onFocus={resetInvalid}
|
||||
onKeyDown={handleKeyDown}
|
||||
disabled={isSigningIn}
|
||||
ref={emailInputRef}
|
||||
/>
|
||||
<InputWithIcon
|
||||
className={`mb-2 ${isInvalid ? 'border-dark-red' : null}`}
|
||||
icon="password"
|
||||
inputType={showPassword ? 'text' : 'password'}
|
||||
placeholder="Password"
|
||||
value={password}
|
||||
onChange={handlePasswordChange}
|
||||
onFocus={resetInvalid}
|
||||
onKeyDown={handleKeyDown}
|
||||
disabled={isSigningIn}
|
||||
toggle={{
|
||||
toggleOnIcon: 'eye-off',
|
||||
toggleOffIcon: 'eye',
|
||||
title: 'Show password',
|
||||
toggled: showPassword,
|
||||
onClick: setShowPassword,
|
||||
}}
|
||||
ref={passwordInputRef}
|
||||
/>
|
||||
{isInvalid ? (
|
||||
<div className="color-dark-red my-2">
|
||||
Invalid email or password.
|
||||
</div>
|
||||
) : null}
|
||||
<Button
|
||||
className="btn-w-full mt-1 mb-3"
|
||||
label={isSigningIn ? 'Signing in...' : 'Sign in'}
|
||||
type="primary"
|
||||
onClick={handleSignInFormSubmit}
|
||||
disabled={isSigningIn}
|
||||
/>
|
||||
<Checkbox
|
||||
name="is-ephemeral"
|
||||
label="Stay signed in"
|
||||
checked={!isEphemeral}
|
||||
disabled={isSigningIn}
|
||||
onChange={handleEphemeralChange}
|
||||
/>
|
||||
{notesAndTagsCount > 0 ? (
|
||||
<Checkbox
|
||||
name="should-merge-local"
|
||||
label={`Merge local data (${notesAndTagsCount} notes and tags)`}
|
||||
checked={shouldMergeLocal}
|
||||
disabled={isSigningIn}
|
||||
onChange={handleShouldMergeChange}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</form>
|
||||
<div className="h-1px my-2 bg-border"></div>
|
||||
<AdvancedOptions
|
||||
appState={appState}
|
||||
application={application}
|
||||
disabled={isSigningIn}
|
||||
>
|
||||
<div className="flex justify-between items-center mb-1">
|
||||
<Checkbox
|
||||
name="use-strict-signin"
|
||||
label="Use strict sign-in"
|
||||
checked={isStrictSignin}
|
||||
disabled={isSigningIn}
|
||||
onChange={handleStrictSigninChange}
|
||||
/>
|
||||
<a
|
||||
href="https://standardnotes.com/help/security"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
title="Learn more"
|
||||
>
|
||||
<Icon type="info" className="color-neutral" />
|
||||
</a>
|
||||
</div>
|
||||
</AdvancedOptions>
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
@@ -1,6 +1,5 @@
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { AppState } from '@/ui_models/app_state';
|
||||
import { PasswordWizardType } from '@/types';
|
||||
import { WebApplication } from '@/ui_models/application';
|
||||
import { User } from '@standardnotes/snjs/dist/@types/services/api/responses';
|
||||
|
||||
@@ -10,22 +9,12 @@ type Props = {
|
||||
}
|
||||
|
||||
const User = observer(({
|
||||
appState,
|
||||
application,
|
||||
}: Props) => {
|
||||
const { server, closeAccountMenu } = appState.accountMenu;
|
||||
appState,
|
||||
application,
|
||||
}: Props) => {
|
||||
const { server } = appState.accountMenu;
|
||||
const user = application.getUser();
|
||||
|
||||
const openPasswordWizard = () => {
|
||||
closeAccountMenu();
|
||||
application.presentPasswordWizard(PasswordWizardType.ChangePassword);
|
||||
};
|
||||
|
||||
const openSessionsModal = () => {
|
||||
closeAccountMenu();
|
||||
appState.openSessionsModal();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="sk-panel-section">
|
||||
{appState.sync.errorMessage && (
|
||||
@@ -56,12 +45,6 @@ const User = observer(({
|
||||
</div>
|
||||
</div>
|
||||
<div className="sk-panel-row" />
|
||||
<a className="sk-a info sk-panel-row condensed" onClick={openPasswordWizard}>
|
||||
Change Password
|
||||
</a>
|
||||
<a className="sk-a info sk-panel-row condensed" onClick={openSessionsModal}>
|
||||
Manage Sessions
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -2,90 +2,115 @@ import { observer } from 'mobx-react-lite';
|
||||
import { toDirective } from '@/components/utils';
|
||||
import { AppState } from '@/ui_models/app_state';
|
||||
import { WebApplication } from '@/ui_models/application';
|
||||
import { ConfirmSignoutContainer } from '@/components/ConfirmSignoutModal';
|
||||
import Authentication from '@/components/AccountMenu/Authentication';
|
||||
import Footer from '@/components/AccountMenu/Footer';
|
||||
import User from '@/components/AccountMenu/User';
|
||||
import Encryption from '@/components/AccountMenu/Encryption';
|
||||
import Protections from '@/components/AccountMenu/Protections';
|
||||
import PasscodeLock from '@/components/AccountMenu/PasscodeLock';
|
||||
import DataBackup from '@/components/AccountMenu/DataBackup';
|
||||
import ErrorReporting from '@/components/AccountMenu/ErrorReporting';
|
||||
import { useEffect } from 'preact/hooks';
|
||||
import { useState } from 'preact/hooks';
|
||||
import { GeneralAccountMenu } from './GeneralAccountMenu';
|
||||
import { FunctionComponent } from 'preact';
|
||||
import { SignInPane } from './SignIn';
|
||||
import { CreateAccount } from './CreateAccount';
|
||||
import { ConfirmSignoutContainer } from '../ConfirmSignoutModal';
|
||||
import { ConfirmPassword } from './ConfirmPassword';
|
||||
|
||||
export enum AccountMenuPane {
|
||||
GeneralMenu,
|
||||
SignIn,
|
||||
Register,
|
||||
ConfirmPassword,
|
||||
}
|
||||
|
||||
type Props = {
|
||||
appState: AppState;
|
||||
application: WebApplication;
|
||||
};
|
||||
|
||||
const AccountMenu = observer(({ application, appState }: Props) => {
|
||||
const {
|
||||
show: showAccountMenu,
|
||||
showLogin,
|
||||
showRegister,
|
||||
setShowLogin,
|
||||
setShowRegister,
|
||||
closeAccountMenu
|
||||
} = appState.accountMenu;
|
||||
type PaneSelectorProps = Props & {
|
||||
menuPane: AccountMenuPane;
|
||||
setMenuPane: (pane: AccountMenuPane) => void;
|
||||
closeMenu: () => void;
|
||||
};
|
||||
|
||||
const user = application.getUser();
|
||||
const MenuPaneSelector: FunctionComponent<PaneSelectorProps> = observer(
|
||||
({ application, appState, menuPane, setMenuPane, closeMenu }) => {
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
// Reset "Login" and "Registration" sections state when hiding account menu,
|
||||
// so the next time account menu is opened these sections are closed
|
||||
if (!showAccountMenu) {
|
||||
setShowLogin(false);
|
||||
setShowRegister(false);
|
||||
}
|
||||
}, [setShowLogin, setShowRegister, showAccountMenu]);
|
||||
|
||||
return (
|
||||
<div className="sn-component">
|
||||
<div id="account-panel" className="sk-panel">
|
||||
<div className="sk-panel-header">
|
||||
<div className="sk-panel-header-title">Account</div>
|
||||
<a className="sk-a info close-button" onClick={closeAccountMenu}>Close</a>
|
||||
</div>
|
||||
<div className="sk-panel-content">
|
||||
<Authentication
|
||||
application={application}
|
||||
switch (menuPane) {
|
||||
case AccountMenuPane.GeneralMenu:
|
||||
return (
|
||||
<GeneralAccountMenu
|
||||
appState={appState}
|
||||
application={application}
|
||||
setMenuPane={setMenuPane}
|
||||
closeMenu={closeMenu}
|
||||
/>
|
||||
);
|
||||
case AccountMenuPane.SignIn:
|
||||
return (
|
||||
<SignInPane
|
||||
appState={appState}
|
||||
application={application}
|
||||
setMenuPane={setMenuPane}
|
||||
/>
|
||||
);
|
||||
case AccountMenuPane.Register:
|
||||
return (
|
||||
<CreateAccount
|
||||
appState={appState}
|
||||
application={application}
|
||||
setMenuPane={setMenuPane}
|
||||
email={email}
|
||||
setEmail={setEmail}
|
||||
password={password}
|
||||
setPassword={setPassword}
|
||||
/>
|
||||
);
|
||||
case AccountMenuPane.ConfirmPassword:
|
||||
return (
|
||||
<ConfirmPassword
|
||||
appState={appState}
|
||||
application={application}
|
||||
setMenuPane={setMenuPane}
|
||||
email={email}
|
||||
password={password}
|
||||
setPassword={setPassword}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const AccountMenu: FunctionComponent<Props> = observer(
|
||||
({ application, appState }) => {
|
||||
const {
|
||||
currentPane,
|
||||
setCurrentPane,
|
||||
shouldAnimateCloseMenu,
|
||||
closeAccountMenu,
|
||||
} = appState.accountMenu;
|
||||
|
||||
return (
|
||||
<div className="sn-component">
|
||||
<div
|
||||
className={`sn-account-menu sn-dropdown ${
|
||||
shouldAnimateCloseMenu
|
||||
? 'slide-up-animation'
|
||||
: 'sn-dropdown--animated'
|
||||
} min-w-80 max-h-120 max-w-xs flex flex-col py-2 overflow-y-auto absolute`}
|
||||
>
|
||||
<MenuPaneSelector
|
||||
appState={appState}
|
||||
application={application}
|
||||
menuPane={currentPane}
|
||||
setMenuPane={setCurrentPane}
|
||||
closeMenu={closeAccountMenu}
|
||||
/>
|
||||
{!showLogin && !showRegister && (
|
||||
<div>
|
||||
{user && (
|
||||
<User
|
||||
application={application}
|
||||
appState={appState}
|
||||
/>
|
||||
)}
|
||||
<Encryption appState={appState} />
|
||||
<Protections application={application} />
|
||||
<PasscodeLock
|
||||
application={application}
|
||||
appState={appState}
|
||||
/>
|
||||
<DataBackup
|
||||
application={application}
|
||||
appState={appState}
|
||||
/>
|
||||
<ErrorReporting appState={appState} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<ConfirmSignoutContainer
|
||||
application={application}
|
||||
appState={appState}
|
||||
/>
|
||||
<Footer
|
||||
application={application}
|
||||
appState={appState}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export const AccountMenuDirective = toDirective<Props>(
|
||||
AccountMenu
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export const AccountMenuDirective = toDirective<Props>(AccountMenu);
|
||||
|
||||
@@ -1,27 +1,34 @@
|
||||
import { JSXInternal } from 'preact/src/jsx';
|
||||
import TargetedEvent = JSXInternal.TargetedEvent;
|
||||
import TargetedMouseEvent = JSXInternal.TargetedMouseEvent;
|
||||
|
||||
import { FunctionComponent } from 'preact';
|
||||
|
||||
const baseClass = `rounded px-4 py-1.75 font-bold text-sm fit-content`;
|
||||
|
||||
const normalClass = `${baseClass} bg-default color-text border-solid border-gray-300 border-1 \
|
||||
focus:bg-contrast hover:bg-contrast`;
|
||||
const primaryClass = `${baseClass} no-border bg-info color-info-contrast hover:brightness-130 \
|
||||
focus:brightness-130`;
|
||||
type ButtonType = 'normal' | 'primary' | 'danger';
|
||||
|
||||
const buttonClasses: { [type in ButtonType]: string } = {
|
||||
normal: `${baseClass} bg-default color-text border-neutral border-solid border-gray-300 border-1 focus:bg-contrast hover:bg-contrast`,
|
||||
primary: `${baseClass} no-border bg-info color-info-contrast hover:brightness-130 focus:brightness-130`,
|
||||
danger: `${baseClass} bg-default color-danger border-neutral border-solid border-gray-300 border-1 focus:bg-contrast hover:bg-contrast`,
|
||||
};
|
||||
|
||||
export const Button: FunctionComponent<{
|
||||
className?: string;
|
||||
type: 'normal' | 'primary';
|
||||
type: ButtonType;
|
||||
label: string;
|
||||
onClick: () => void;
|
||||
onClick: (event: TargetedEvent<HTMLFormElement> | TargetedMouseEvent<HTMLButtonElement>) => void;
|
||||
disabled?: boolean;
|
||||
}> = ({ type, label, className = '', onClick, disabled = false }) => {
|
||||
const buttonClass = type === 'primary' ? primaryClass : normalClass;
|
||||
const buttonClass = buttonClasses[type];
|
||||
const cursorClass = disabled ? 'cursor-default' : 'cursor-pointer';
|
||||
|
||||
return (
|
||||
<button
|
||||
className={`${buttonClass} ${cursorClass} ${className}`}
|
||||
onClick={(e) => {
|
||||
onClick();
|
||||
onClick(e);
|
||||
e.preventDefault();
|
||||
}}
|
||||
disabled={disabled}
|
||||
|
||||
32
app/assets/javascripts/components/Checkbox.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import { FunctionComponent } from 'preact';
|
||||
|
||||
type CheckboxProps = {
|
||||
name: string;
|
||||
checked: boolean;
|
||||
onChange: (e: Event) => void;
|
||||
disabled?: boolean;
|
||||
label: string;
|
||||
};
|
||||
|
||||
export const Checkbox: FunctionComponent<CheckboxProps> = ({
|
||||
name,
|
||||
checked,
|
||||
onChange,
|
||||
disabled,
|
||||
label,
|
||||
}) => {
|
||||
return (
|
||||
<label htmlFor={name} className="flex items-center fit-content mb-2">
|
||||
<input
|
||||
className="mr-2"
|
||||
type="checkbox"
|
||||
name={name}
|
||||
id={name}
|
||||
checked={checked}
|
||||
onChange={onChange}
|
||||
disabled={disabled}
|
||||
/>
|
||||
{label}
|
||||
</label>
|
||||
);
|
||||
};
|
||||
@@ -26,7 +26,7 @@ const ConfirmSignoutModal = observer(({ application, appState }: Props) => {
|
||||
const [deleteLocalBackups, setDeleteLocalBackups] = useState(false);
|
||||
|
||||
const cancelRef = useRef<HTMLButtonElement>();
|
||||
function close() {
|
||||
function closeDialog() {
|
||||
appState.accountMenu.setSigningOut(false);
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ const ConfirmSignoutModal = observer(({ application, appState }: Props) => {
|
||||
}, [appState.accountMenu.signingOut, application.bridge]);
|
||||
|
||||
return (
|
||||
<AlertDialog onDismiss={close} leastDestructiveRef={cancelRef}>
|
||||
<AlertDialog onDismiss={closeDialog} leastDestructiveRef={cancelRef}>
|
||||
<div className="sk-modal-content">
|
||||
<div className="sn-component">
|
||||
<div className="sk-panel">
|
||||
@@ -83,7 +83,7 @@ const ConfirmSignoutModal = observer(({ application, appState }: Props) => {
|
||||
<button
|
||||
className="sn-button small neutral"
|
||||
ref={cancelRef}
|
||||
onClick={close}
|
||||
onClick={closeDialog}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
@@ -95,7 +95,7 @@ const ConfirmSignoutModal = observer(({ application, appState }: Props) => {
|
||||
} else {
|
||||
application.signOut();
|
||||
}
|
||||
close();
|
||||
closeDialog();
|
||||
}}
|
||||
>
|
||||
{application.hasAccount()
|
||||
|
||||
35
app/assets/javascripts/components/ConfirmationDialog.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import { ComponentChildren, FunctionComponent } from 'preact';
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogDescription,
|
||||
AlertDialogLabel,
|
||||
} from '@reach/alert-dialog';
|
||||
import { useRef } from 'preact/hooks';
|
||||
|
||||
export const ConfirmationDialog: FunctionComponent<{
|
||||
title: string | ComponentChildren;
|
||||
}> = ({ title, children }) => {
|
||||
const ldRef = useRef<HTMLButtonElement>();
|
||||
|
||||
return (
|
||||
<AlertDialog leastDestructiveRef={ldRef}>
|
||||
{/* sn-component is focusable by default, but doesn't stretch to child width
|
||||
resulting in a badly focused dialog. Utility classes are not available
|
||||
at the sn-component level, only below it. tabIndex -1 disables focus
|
||||
and enables it on the child component */}
|
||||
<div tabIndex={-1} className="sn-component">
|
||||
<div
|
||||
tabIndex={0}
|
||||
className="max-w-89 bg-default rounded shadow-overlay focus:padded-ring-info px-9 py-9 flex flex-col items-center"
|
||||
>
|
||||
<AlertDialogLabel>{title}</AlertDialogLabel>
|
||||
<div className="min-h-2" />
|
||||
|
||||
<AlertDialogDescription className="flex flex-col items-center">
|
||||
{children}
|
||||
</AlertDialogDescription>
|
||||
</div>
|
||||
</div>
|
||||
</AlertDialog>
|
||||
);
|
||||
};
|
||||
@@ -1,42 +1,69 @@
|
||||
import { FunctionalComponent, ComponentChild } from 'preact';
|
||||
import { HtmlInputTypes } from '@/enums';
|
||||
|
||||
interface Props {
|
||||
type?: HtmlInputTypes;
|
||||
className?: string;
|
||||
disabled?: boolean;
|
||||
left?: ComponentChild[];
|
||||
right?: ComponentChild[];
|
||||
text?: string;
|
||||
placeholder?: string;
|
||||
onChange?: (text: string) => void;
|
||||
autocomplete?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Input that can be decorated on the left and right side
|
||||
*/
|
||||
export const DecoratedInput: FunctionalComponent<Props> = ({
|
||||
type = 'text',
|
||||
className = '',
|
||||
disabled = false,
|
||||
left,
|
||||
right,
|
||||
text,
|
||||
placeholder = '',
|
||||
onChange,
|
||||
autocomplete = false,
|
||||
}) => {
|
||||
const base =
|
||||
'rounded py-1.5 px-3 text-input my-1 h-8 flex flex-row items-center gap-4';
|
||||
const baseClasses =
|
||||
'rounded py-1.5 px-3 text-input my-1 h-8 flex flex-row items-center bg-contrast';
|
||||
const stateClasses = disabled
|
||||
? 'no-border bg-grey-5'
|
||||
? 'no-border'
|
||||
: 'border-solid border-1 border-gray-300';
|
||||
const classes = `${base} ${stateClasses} ${className}`;
|
||||
const classes = `${baseClasses} ${stateClasses} ${className}`;
|
||||
|
||||
const inputBaseClasses = 'w-full no-border color-text focus:shadow-none bg-contrast';
|
||||
const inputStateClasses = disabled ? 'overflow-ellipsis' : '';
|
||||
return (
|
||||
<div className={`${classes} focus-within:ring-info`}>
|
||||
{left}
|
||||
{left?.map((leftChild) => (
|
||||
<>
|
||||
{leftChild}
|
||||
<div className="min-w-2 min-h-1" />
|
||||
</>
|
||||
))}
|
||||
<div className="flex-grow">
|
||||
<input
|
||||
type="text"
|
||||
className="w-full no-border color-black focus:shadow-none"
|
||||
type={type}
|
||||
className={`${inputBaseClasses} ${inputStateClasses}`}
|
||||
disabled={disabled}
|
||||
value={text}
|
||||
placeholder={placeholder}
|
||||
onChange={(e) =>
|
||||
onChange && onChange((e.target as HTMLInputElement).value)
|
||||
}
|
||||
data-lpignore={type !== 'password' ? true : false}
|
||||
autocomplete={autocomplete ? 'on' : 'off'}
|
||||
/>
|
||||
</div>
|
||||
{right}
|
||||
{right?.map((rightChild) => (
|
||||
<>
|
||||
<div className="min-w-3 min-h-1" />
|
||||
{rightChild}
|
||||
</>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
119
app/assets/javascripts/components/Dropdown.tsx
Normal file
@@ -0,0 +1,119 @@
|
||||
import {
|
||||
ListboxArrow,
|
||||
ListboxButton,
|
||||
ListboxInput,
|
||||
ListboxList,
|
||||
ListboxOption,
|
||||
ListboxPopover,
|
||||
} from '@reach/listbox';
|
||||
import VisuallyHidden from '@reach/visually-hidden';
|
||||
import { FunctionComponent } from 'preact';
|
||||
import { IconType, Icon } from './Icon';
|
||||
import { useState } from 'preact/hooks';
|
||||
|
||||
export type DropdownItem = {
|
||||
icon?: IconType;
|
||||
label: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
type DropdownProps = {
|
||||
id: string;
|
||||
label: string;
|
||||
items: DropdownItem[];
|
||||
defaultValue: string;
|
||||
onChange: (value: string) => void;
|
||||
};
|
||||
|
||||
type ListboxButtonProps = {
|
||||
icon?: IconType;
|
||||
value: string | null;
|
||||
label: string;
|
||||
isExpanded: boolean;
|
||||
};
|
||||
|
||||
const CustomDropdownButton: FunctionComponent<ListboxButtonProps> = ({
|
||||
label,
|
||||
isExpanded,
|
||||
icon,
|
||||
}) => (
|
||||
<>
|
||||
<div className="sn-dropdown-button-label">
|
||||
{icon ? (
|
||||
<div className="flex mr-2">
|
||||
<Icon type={icon} className="sn-icon--small" />
|
||||
</div>
|
||||
) : null}
|
||||
<div className="dropdown-selected-label">{label}</div>
|
||||
</div>
|
||||
<ListboxArrow
|
||||
className={`sn-dropdown-arrow ${
|
||||
isExpanded ? 'sn-dropdown-arrow-flipped' : ''
|
||||
}`}
|
||||
>
|
||||
<Icon type="menu-arrow-down" className="sn-icon--small color-grey-1" />
|
||||
</ListboxArrow>
|
||||
</>
|
||||
);
|
||||
|
||||
export const Dropdown: FunctionComponent<DropdownProps> = ({
|
||||
id,
|
||||
label,
|
||||
items,
|
||||
defaultValue,
|
||||
onChange,
|
||||
}) => {
|
||||
const [value, setValue] = useState(defaultValue);
|
||||
|
||||
const labelId = `${id}-label`;
|
||||
|
||||
const handleChange = (value: string) => {
|
||||
setValue(value);
|
||||
onChange(value);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<VisuallyHidden id={labelId}>{label}</VisuallyHidden>
|
||||
<ListboxInput
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
aria-labelledby={labelId}
|
||||
>
|
||||
<ListboxButton
|
||||
className="sn-dropdown-button"
|
||||
children={({ value, label, isExpanded }) => {
|
||||
const current = items.find((item) => item.value === value);
|
||||
const icon = current ? current?.icon : null;
|
||||
return CustomDropdownButton({
|
||||
value,
|
||||
label,
|
||||
isExpanded,
|
||||
...(icon ? { icon } : null),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<ListboxPopover className="sn-dropdown sn-dropdown-popover">
|
||||
<div className="sn-component">
|
||||
<ListboxList>
|
||||
{items.map((item) => (
|
||||
<ListboxOption
|
||||
className="sn-dropdown-item"
|
||||
value={item.value}
|
||||
label={item.label}
|
||||
>
|
||||
{item.icon ? (
|
||||
<div className="flex mr-3">
|
||||
<Icon type={item.icon} className="sn-icon--small" />
|
||||
</div>
|
||||
) : null}
|
||||
<div className="text-input">{item.label}</div>
|
||||
</ListboxOption>
|
||||
))}
|
||||
</ListboxList>
|
||||
</div>
|
||||
</ListboxPopover>
|
||||
</ListboxInput>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,4 +1,5 @@
|
||||
import PencilOffIcon from '../../icons/ic-pencil-off.svg';
|
||||
import PlainTextIcon from '../../icons/ic-text-paragraph.svg';
|
||||
import RichTextIcon from '../../icons/ic-text-rich.svg';
|
||||
import TrashIcon from '../../icons/ic-trash.svg';
|
||||
import PinIcon from '../../icons/ic-pin.svg';
|
||||
@@ -13,6 +14,12 @@ import PasswordIcon from '../../icons/ic-textbox-password.svg';
|
||||
import TrashSweepIcon from '../../icons/ic-trash-sweep.svg';
|
||||
import MoreIcon from '../../icons/ic-more.svg';
|
||||
import TuneIcon from '../../icons/ic-tune.svg';
|
||||
import MenuArrowDownIcon from '../../icons/ic-menu-arrow-down.svg';
|
||||
import AuthenticatorIcon from '../../icons/ic-authenticator.svg';
|
||||
import SpreadsheetsIcon from '../../icons/ic-spreadsheets.svg';
|
||||
import TasksIcon from '../../icons/ic-tasks.svg';
|
||||
import MarkdownIcon from '../../icons/ic-markdown.svg';
|
||||
import CodeIcon from '../../icons/ic-code.svg';
|
||||
|
||||
import AccessibilityIcon from '../../icons/ic-accessibility.svg';
|
||||
import HelpIcon from '../../icons/ic-help.svg';
|
||||
@@ -26,13 +33,46 @@ import UserIcon from '../../icons/ic-user.svg';
|
||||
import CopyIcon from '../../icons/ic-copy.svg';
|
||||
import DownloadIcon from '../../icons/ic-download.svg';
|
||||
import InfoIcon from '../../icons/ic-info.svg';
|
||||
import CheckIcon from '../../icons/ic-check.svg';
|
||||
import CheckBoldIcon from '../../icons/ic-check-bold.svg';
|
||||
import AccountCircleIcon from '../../icons/ic-account-circle.svg';
|
||||
import CloudOffIcon from '../../icons/ic-cloud-off.svg';
|
||||
import SignInIcon from '../../icons/ic-signin.svg';
|
||||
import SignOutIcon from '../../icons/ic-signout.svg';
|
||||
import CheckCircleIcon from '../../icons/ic-check-circle.svg';
|
||||
import SyncIcon from '../../icons/ic-sync.svg';
|
||||
import ArrowLeftIcon from '../../icons/ic-arrow-left.svg';
|
||||
import ChevronDownIcon from '../../icons/ic-chevron-down.svg';
|
||||
import EmailIcon from '../../icons/ic-email.svg';
|
||||
import ServerIcon from '../../icons/ic-server.svg';
|
||||
import EyeIcon from '../../icons/ic-eye.svg';
|
||||
import EyeOffIcon from '../../icons/ic-eye-off.svg';
|
||||
import LockIcon from '../../icons/ic-lock.svg';
|
||||
|
||||
import { toDirective } from './utils';
|
||||
import { FunctionalComponent } from 'preact';
|
||||
|
||||
const ICONS = {
|
||||
lock: LockIcon,
|
||||
eye: EyeIcon,
|
||||
'eye-off': EyeOffIcon,
|
||||
server: ServerIcon,
|
||||
email: EmailIcon,
|
||||
'chevron-down': ChevronDownIcon,
|
||||
'arrow-left': ArrowLeftIcon,
|
||||
sync: SyncIcon,
|
||||
'check-circle': CheckCircleIcon,
|
||||
signIn: SignInIcon,
|
||||
signOut: SignOutIcon,
|
||||
'cloud-off': CloudOffIcon,
|
||||
'pencil-off': PencilOffIcon,
|
||||
'plain-text': PlainTextIcon,
|
||||
'rich-text': RichTextIcon,
|
||||
code: CodeIcon,
|
||||
markdown: MarkdownIcon,
|
||||
authenticator: AuthenticatorIcon,
|
||||
spreadsheets: SpreadsheetsIcon,
|
||||
tasks: TasksIcon,
|
||||
trash: TrashIcon,
|
||||
pin: PinIcon,
|
||||
unpin: UnpinIcon,
|
||||
@@ -58,6 +98,10 @@ const ICONS = {
|
||||
copy: CopyIcon,
|
||||
download: DownloadIcon,
|
||||
info: InfoIcon,
|
||||
check: CheckIcon,
|
||||
'check-bold': CheckBoldIcon,
|
||||
'account-circle': AccountCircleIcon,
|
||||
'menu-arrow-down': MenuArrowDownIcon,
|
||||
};
|
||||
|
||||
export type IconType = keyof typeof ICONS;
|
||||
|
||||
@@ -10,6 +10,17 @@ interface Props {
|
||||
className?: string;
|
||||
|
||||
icon: IconType;
|
||||
|
||||
iconClassName?: string;
|
||||
|
||||
/**
|
||||
* Button tooltip
|
||||
*/
|
||||
title: string;
|
||||
|
||||
focusable: boolean;
|
||||
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -18,21 +29,26 @@ interface Props {
|
||||
*/
|
||||
export const IconButton: FunctionComponent<Props> = ({
|
||||
onClick,
|
||||
className,
|
||||
className = '',
|
||||
icon,
|
||||
title,
|
||||
focusable,
|
||||
iconClassName = '',
|
||||
disabled = false,
|
||||
}) => {
|
||||
const click = (e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
onClick();
|
||||
};
|
||||
const focusableClass = focusable ? '' : 'focus:shadow-none';
|
||||
return (
|
||||
<button
|
||||
className={`no-border cursor-pointer bg-transparent hover:brightness-130 p-0 ${
|
||||
className ?? ''
|
||||
}`}
|
||||
title={title}
|
||||
className={`no-border cursor-pointer bg-transparent flex flex-row items-center hover:brightness-130 p-0 ${focusableClass} ${className}`}
|
||||
onClick={click}
|
||||
disabled={disabled}
|
||||
>
|
||||
<Icon type={icon} />
|
||||
<Icon type={icon} className={iconClassName} />
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -11,9 +11,9 @@ export const Input: FunctionalComponent<Props> = ({
|
||||
disabled = false,
|
||||
text,
|
||||
}) => {
|
||||
const base = `rounded py-1.5 px-3 text-input my-1 h-8`;
|
||||
const base = `rounded py-1.5 px-3 text-input my-1 h-8 bg-contrast`;
|
||||
const stateClasses = disabled
|
||||
? 'no-border bg-grey-5'
|
||||
? 'no-border'
|
||||
: 'border-solid border-1 border-gray-300';
|
||||
const classes = `${base} ${stateClasses} ${className}`;
|
||||
return (
|
||||
|
||||
89
app/assets/javascripts/components/InputWithIcon.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
import { FunctionComponent, Ref } from 'preact';
|
||||
import { JSXInternal } from 'preact/src/jsx';
|
||||
import { forwardRef } from 'preact/compat';
|
||||
import { Icon, IconType } from './Icon';
|
||||
import { IconButton } from './IconButton';
|
||||
|
||||
type ToggleProps = {
|
||||
toggleOnIcon: IconType;
|
||||
toggleOffIcon: IconType;
|
||||
title: string;
|
||||
toggled: boolean;
|
||||
onClick: (toggled: boolean) => void;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
icon: IconType;
|
||||
inputType: 'text' | 'email' | 'password';
|
||||
className?: string;
|
||||
iconClassName?: string;
|
||||
value: string | undefined;
|
||||
onChange: JSXInternal.GenericEventHandler<HTMLInputElement>;
|
||||
onFocus?: JSXInternal.GenericEventHandler<HTMLInputElement>;
|
||||
onKeyDown?: JSXInternal.KeyboardEventHandler<HTMLInputElement>;
|
||||
disabled?: boolean;
|
||||
placeholder: string;
|
||||
toggle?: ToggleProps;
|
||||
};
|
||||
|
||||
const DISABLED_CLASSNAME = 'bg-grey-5 cursor-not-allowed';
|
||||
|
||||
export const InputWithIcon: FunctionComponent<Props> = forwardRef(
|
||||
(
|
||||
{
|
||||
icon,
|
||||
inputType,
|
||||
className,
|
||||
iconClassName,
|
||||
value,
|
||||
onChange,
|
||||
onFocus,
|
||||
onKeyDown,
|
||||
disabled,
|
||||
toggle,
|
||||
placeholder,
|
||||
},
|
||||
ref: Ref<HTMLInputElement>
|
||||
) => {
|
||||
const handleToggle = () => {
|
||||
if (toggle) toggle.onClick(!toggle?.toggled);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`flex items-stretch position-relative bg-default border-1 border-solid border-neutral rounded focus-within:ring-info overflow-hidden ${
|
||||
disabled ? DISABLED_CLASSNAME : ''
|
||||
} ${className}`}
|
||||
>
|
||||
<div className="flex px-2 py-1.5">
|
||||
<Icon type={icon} className={`color-grey-1 ${iconClassName}`} />
|
||||
</div>
|
||||
<input
|
||||
type={inputType}
|
||||
onFocus={onFocus}
|
||||
onChange={onChange}
|
||||
onKeyDown={onKeyDown}
|
||||
value={value}
|
||||
className={`pr-2 w-full border-0 focus:shadow-none ${
|
||||
disabled ? DISABLED_CLASSNAME : ''
|
||||
}`}
|
||||
disabled={disabled}
|
||||
placeholder={placeholder}
|
||||
ref={ref}
|
||||
/>
|
||||
{toggle ? (
|
||||
<div className="flex items-center justify-center px-2">
|
||||
<IconButton
|
||||
className="w-5 h-5 justify-center sk-circle hover:bg-grey-4"
|
||||
icon={toggle.toggled ? toggle.toggleOnIcon : toggle.toggleOffIcon}
|
||||
iconClassName="sn-icon--small"
|
||||
title={toggle.title}
|
||||
onClick={handleToggle}
|
||||
focusable={true}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
@@ -54,7 +54,7 @@ export const NoteTag = observer(({ appState, tag }: Props) => {
|
||||
const getTabIndex = () => {
|
||||
if (focusedTagUuid) {
|
||||
return focusedTagUuid === tag.uuid ? 0 : -1;
|
||||
}
|
||||
}
|
||||
if (autocompleteInputFocused) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ const NotesContextMenu = observer(({ application, appState }: Props) => {
|
||||
);
|
||||
|
||||
useCloseOnClickOutside(
|
||||
contextMenuRef,
|
||||
contextMenuRef,
|
||||
(open: boolean) => appState.notes.setContextMenuOpen(open)
|
||||
);
|
||||
|
||||
|
||||
80
app/assets/javascripts/components/OtherSessionsSignOut.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
import { useRef, useState } from 'preact/hooks';
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogDescription,
|
||||
AlertDialogLabel,
|
||||
} from '@reach/alert-dialog';
|
||||
import { WebApplication } from '@/ui_models/application';
|
||||
import { AppState } from '@/ui_models/app_state';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { FunctionComponent } from 'preact';
|
||||
|
||||
type Props = {
|
||||
application: WebApplication;
|
||||
appState: AppState;
|
||||
};
|
||||
|
||||
export const OtherSessionsSignOutContainer = observer((props: Props) => {
|
||||
if (!props.appState.accountMenu.otherSessionsSignOut) {
|
||||
return null;
|
||||
}
|
||||
return <ConfirmOtherSessionsSignOut {...props} />;
|
||||
});
|
||||
|
||||
const ConfirmOtherSessionsSignOut = observer(
|
||||
({ application, appState }: Props) => {
|
||||
const cancelRef = useRef<HTMLButtonElement>();
|
||||
function closeDialog() {
|
||||
appState.accountMenu.setOtherSessionsSignOut(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<AlertDialog onDismiss={closeDialog} leastDestructiveRef={cancelRef}>
|
||||
<div className="sk-modal-content">
|
||||
<div className="sn-component">
|
||||
<div className="sk-panel">
|
||||
<div className="sk-panel-content">
|
||||
<div className="sk-panel-section">
|
||||
<AlertDialogLabel className="sk-h3 sk-panel-section-title capitalize">
|
||||
End all other sessions?
|
||||
</AlertDialogLabel>
|
||||
<AlertDialogDescription className="sk-panel-row">
|
||||
<p className="color-foreground">
|
||||
This action will sign out all other devices signed into
|
||||
your account, and remove your data from those devices when
|
||||
they next regain connection to the internet. You may sign
|
||||
back in on those devices at any time.
|
||||
</p>
|
||||
</AlertDialogDescription>
|
||||
<div className="flex my-1 mt-4">
|
||||
<button
|
||||
className="sn-button small neutral"
|
||||
ref={cancelRef}
|
||||
onClick={closeDialog}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
className="sn-button small danger ml-2"
|
||||
onClick={() => {
|
||||
application.revokeAllOtherSessions();
|
||||
closeDialog();
|
||||
application.alertService.alert(
|
||||
'You have successfully revoked your sessions from other devices.',
|
||||
undefined,
|
||||
'Finish'
|
||||
);
|
||||
}}
|
||||
>
|
||||
End Sessions
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AlertDialog>
|
||||
);
|
||||
}
|
||||
);
|
||||
@@ -26,12 +26,12 @@ type Session = RemoteSession & {
|
||||
function useSessions(
|
||||
application: SNApplication
|
||||
): [
|
||||
Session[],
|
||||
() => void,
|
||||
boolean,
|
||||
(uuid: UuidString) => Promise<void>,
|
||||
string
|
||||
] {
|
||||
Session[],
|
||||
() => void,
|
||||
boolean,
|
||||
(uuid: UuidString) => Promise<void>,
|
||||
string
|
||||
] {
|
||||
const [sessions, setSessions] = useState<Session[]>([]);
|
||||
const [lastRefreshDate, setLastRefreshDate] = useState(Date.now());
|
||||
const [refreshing, setRefreshing] = useState(true);
|
||||
@@ -240,7 +240,7 @@ const SessionsModal: FunctionComponent<{
|
||||
);
|
||||
};
|
||||
|
||||
const Sessions: FunctionComponent<{
|
||||
export const Sessions: FunctionComponent<{
|
||||
appState: AppState;
|
||||
application: WebApplication;
|
||||
}> = observer(({ appState, application }) => {
|
||||
|
||||
@@ -23,7 +23,7 @@ export const Switch: FunctionalComponent<SwitchProps> = (
|
||||
const className = props.className ?? '';
|
||||
return (
|
||||
<label
|
||||
className={`sn-component flex justify-between items-center cursor-pointer hover:bg-contrast px-3 ${className}`}
|
||||
className={`sn-component flex justify-between items-center cursor-pointer px-3 ${className}`}
|
||||
>
|
||||
{props.children}
|
||||
<CustomCheckboxContainer
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { FunctionalComponent } from 'preact';
|
||||
|
||||
type Props = {
|
||||
classes?: string;
|
||||
}
|
||||
export const HorizontalSeparator: FunctionalComponent<Props> = ({
|
||||
classes = ''
|
||||
}) => {
|
||||
return <hr className={`h-1px w-full bg-border no-border ${classes}`} />;
|
||||
};
|
||||
74
app/assets/javascripts/components/shared/ModalDialog.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import { FunctionComponent } from 'preact';
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogDescription,
|
||||
AlertDialogLabel,
|
||||
} from '@node_modules/@reach/alert-dialog';
|
||||
import { useRef } from '@node_modules/preact/hooks';
|
||||
import { IconButton } from '@/components/IconButton';
|
||||
|
||||
export const ModalDialog: FunctionComponent = ({ children }) => {
|
||||
const ldRef = useRef<HTMLButtonElement>();
|
||||
|
||||
return (
|
||||
<AlertDialog leastDestructiveRef={ldRef}>
|
||||
{/* sn-component is focusable by default, but doesn't stretch to child width
|
||||
resulting in a badly focused dialog. Utility classes are not available
|
||||
at the sn-component level, only below it. tabIndex -1 disables focus
|
||||
and enables it on the child component */}
|
||||
<div tabIndex={-1} className="sn-component">
|
||||
<div
|
||||
tabIndex={0}
|
||||
className="w-160 bg-default rounded shadow-overlay focus:padded-ring-info"
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</AlertDialog>
|
||||
);
|
||||
};
|
||||
|
||||
export const ModalDialogLabel: FunctionComponent<{
|
||||
closeDialog: () => void;
|
||||
}> = ({ children, closeDialog }) => (
|
||||
<AlertDialogLabel className="">
|
||||
<div className="px-4 py-4 flex flex-row items-center">
|
||||
<div className="flex-grow color-text text-lg font-bold">{children}</div>
|
||||
<IconButton
|
||||
focusable={true}
|
||||
title="Close"
|
||||
className="color-neutral h-5 w-5"
|
||||
icon="close"
|
||||
onClick={() => closeDialog()}
|
||||
/>
|
||||
</div>
|
||||
<hr className="h-1px bg-border no-border m-0" />
|
||||
</AlertDialogLabel>
|
||||
);
|
||||
|
||||
export const ModalDialogDescription: FunctionComponent<{ className?: string }> =
|
||||
({ children, className = '' }) => (
|
||||
<AlertDialogDescription
|
||||
className={`px-4 py-4 flex flex-row items-center ${className}`}
|
||||
>
|
||||
{children}
|
||||
</AlertDialogDescription>
|
||||
);
|
||||
|
||||
export const ModalDialogButtons: FunctionComponent = ({ children }) => (
|
||||
<>
|
||||
<hr className="h-1px bg-border no-border m-0" />
|
||||
<div className="px-4 py-4 flex flex-row justify-end items-center">
|
||||
{children != undefined && Array.isArray(children)
|
||||
? children.map((child, idx, arr) => (
|
||||
<>
|
||||
{child}
|
||||
{idx < arr.length - 1 ? <div className="min-w-3" /> : undefined}
|
||||
</>
|
||||
))
|
||||
: children}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
export default ModalDialog;
|
||||
@@ -34,7 +34,7 @@ class ComponentViewCtrl implements ComponentViewScope {
|
||||
private unregisterDesktopObserver!: () => void
|
||||
private issueLoading = false
|
||||
private isDeprecated = false
|
||||
private deprecationMessage = ''
|
||||
private deprecationMessage: string | undefined = undefined
|
||||
private deprecationMessageDismissed = false
|
||||
public reloading = false
|
||||
private expired = false
|
||||
|
||||
@@ -4,53 +4,44 @@ import { SNComponent, SNItem, ComponentArea } from '@standardnotes/snjs';
|
||||
import { isDesktopApplication } from '@/utils';
|
||||
import template from '%/directives/editor-menu.pug';
|
||||
import { PureViewCtrl } from '@Views/abstract/pure_view_ctrl';
|
||||
import { ComponentMutator } from '@standardnotes/snjs';
|
||||
|
||||
interface EditorMenuScope {
|
||||
callback: (component: SNComponent) => void
|
||||
selectedEditorUuid: string
|
||||
currentItem: SNItem
|
||||
application: WebApplication
|
||||
callback: (component: SNComponent) => void;
|
||||
selectedEditorUuid: string;
|
||||
currentItem: SNItem;
|
||||
application: WebApplication;
|
||||
}
|
||||
|
||||
class EditorMenuCtrl extends PureViewCtrl implements EditorMenuScope {
|
||||
|
||||
callback!: () => (component: SNComponent) => void
|
||||
selectedEditorUuid!: string
|
||||
currentItem!: SNItem
|
||||
application!: WebApplication
|
||||
callback!: () => (component: SNComponent) => void;
|
||||
selectedEditorUuid!: string;
|
||||
currentItem!: SNItem;
|
||||
application!: WebApplication;
|
||||
|
||||
/* @ngInject */
|
||||
constructor(
|
||||
$timeout: ng.ITimeoutService,
|
||||
) {
|
||||
constructor($timeout: ng.ITimeoutService) {
|
||||
super($timeout);
|
||||
this.state = {
|
||||
isDesktop: isDesktopApplication()
|
||||
isDesktop: isDesktopApplication(),
|
||||
};
|
||||
}
|
||||
|
||||
public isEditorSelected(editor: SNComponent) {
|
||||
if(!this.selectedEditorUuid) {
|
||||
if (!this.selectedEditorUuid) {
|
||||
return false;
|
||||
}
|
||||
return this.selectedEditorUuid === editor.uuid;
|
||||
}
|
||||
|
||||
public isEditorDefault(editor: SNComponent) {
|
||||
return this.state.defaultEditor?.uuid === editor.uuid;
|
||||
}
|
||||
|
||||
$onInit() {
|
||||
super.$onInit();
|
||||
const editors = this.application.componentManager!.componentsForArea(ComponentArea.Editor)
|
||||
const editors = this.application
|
||||
.componentManager!.componentsForArea(ComponentArea.Editor)
|
||||
.sort((a, b) => {
|
||||
return a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1;
|
||||
});
|
||||
const defaultEditor = editors.filter((e) => e.isDefaultEditor())[0];
|
||||
this.setState({
|
||||
editors: editors,
|
||||
defaultEditor: defaultEditor
|
||||
});
|
||||
}
|
||||
|
||||
@@ -67,46 +58,9 @@ class EditorMenuCtrl extends PureViewCtrl implements EditorMenuScope {
|
||||
});
|
||||
}
|
||||
|
||||
toggleDefaultForEditor(editor: SNComponent) {
|
||||
if (this.state.defaultEditor === editor) {
|
||||
this.removeEditorDefault(editor);
|
||||
} else {
|
||||
this.makeEditorDefault(editor);
|
||||
}
|
||||
}
|
||||
|
||||
offlineAvailableForComponent(component: SNComponent) {
|
||||
return component.local_url && this.state.isDesktop;
|
||||
}
|
||||
|
||||
makeEditorDefault(component: SNComponent) {
|
||||
const currentDefault = this.application.componentManager!
|
||||
.componentsForArea(ComponentArea.Editor)
|
||||
.filter((e) => e.isDefaultEditor())[0];
|
||||
if (currentDefault) {
|
||||
this.application.changeItem(currentDefault.uuid, (m) => {
|
||||
const mutator = m as ComponentMutator;
|
||||
mutator.defaultEditor = false;
|
||||
});
|
||||
}
|
||||
this.application.changeAndSaveItem(component.uuid, (m) => {
|
||||
const mutator = m as ComponentMutator;
|
||||
mutator.defaultEditor = true;
|
||||
});
|
||||
this.setState({
|
||||
defaultEditor: component
|
||||
});
|
||||
}
|
||||
|
||||
removeEditorDefault(component: SNComponent) {
|
||||
this.application.changeAndSaveItem(component.uuid, (m) => {
|
||||
const mutator = m as ComponentMutator;
|
||||
mutator.defaultEditor = false;
|
||||
});
|
||||
this.setState({
|
||||
defaultEditor: null
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class EditorMenu extends WebDirective {
|
||||
@@ -121,7 +75,7 @@ export class EditorMenu extends WebDirective {
|
||||
callback: '&',
|
||||
selectedEditorUuid: '=',
|
||||
currentItem: '=',
|
||||
application: '='
|
||||
application: '=',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,48 +1,52 @@
|
||||
import { WebApplication } from '@/ui_models/application';
|
||||
import { PasswordWizardScope, PasswordWizardType, WebDirective } from './../../types';
|
||||
import {
|
||||
PasswordWizardScope,
|
||||
PasswordWizardType,
|
||||
WebDirective,
|
||||
} from './../../types';
|
||||
import template from '%/directives/password-wizard.pug';
|
||||
import { PureViewCtrl } from '@Views/abstract/pure_view_ctrl';
|
||||
|
||||
const DEFAULT_CONTINUE_TITLE = "Continue";
|
||||
const DEFAULT_CONTINUE_TITLE = 'Continue';
|
||||
enum Steps {
|
||||
PasswordStep = 1,
|
||||
FinishStep = 2
|
||||
FinishStep = 2,
|
||||
}
|
||||
|
||||
type FormData = {
|
||||
currentPassword?: string,
|
||||
newPassword?: string,
|
||||
newPasswordConfirmation?: string,
|
||||
status?: string
|
||||
}
|
||||
currentPassword?: string;
|
||||
newPassword?: string;
|
||||
newPasswordConfirmation?: string;
|
||||
status?: string;
|
||||
};
|
||||
|
||||
type State = {
|
||||
lockContinue: boolean
|
||||
formData: FormData,
|
||||
continueTitle: string,
|
||||
step: Steps,
|
||||
title: string,
|
||||
showSpinner: boolean
|
||||
processing: boolean
|
||||
}
|
||||
lockContinue: boolean;
|
||||
formData: FormData;
|
||||
continueTitle: string;
|
||||
step: Steps;
|
||||
title: string;
|
||||
showSpinner: boolean;
|
||||
processing: boolean;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
type: PasswordWizardType,
|
||||
changePassword: boolean,
|
||||
securityUpdate: boolean
|
||||
}
|
||||
type: PasswordWizardType;
|
||||
changePassword: boolean;
|
||||
securityUpdate: boolean;
|
||||
};
|
||||
|
||||
class PasswordWizardCtrl extends PureViewCtrl<Props, State> implements PasswordWizardScope {
|
||||
$element: JQLite
|
||||
application!: WebApplication
|
||||
type!: PasswordWizardType
|
||||
isContinuing = false
|
||||
class PasswordWizardCtrl
|
||||
extends PureViewCtrl<Props, State>
|
||||
implements PasswordWizardScope
|
||||
{
|
||||
$element: JQLite;
|
||||
application!: WebApplication;
|
||||
type!: PasswordWizardType;
|
||||
isContinuing = false;
|
||||
|
||||
/* @ngInject */
|
||||
constructor(
|
||||
$element: JQLite,
|
||||
$timeout: ng.ITimeoutService,
|
||||
) {
|
||||
constructor($element: JQLite, $timeout: ng.ITimeoutService) {
|
||||
super($timeout);
|
||||
this.$element = $element;
|
||||
this.registerWindowUnloadStopper();
|
||||
@@ -53,13 +57,13 @@ class PasswordWizardCtrl extends PureViewCtrl<Props, State> implements PasswordW
|
||||
this.initProps({
|
||||
type: this.type,
|
||||
changePassword: this.type === PasswordWizardType.ChangePassword,
|
||||
securityUpdate: this.type === PasswordWizardType.AccountUpgrade
|
||||
securityUpdate: this.type === PasswordWizardType.AccountUpgrade,
|
||||
});
|
||||
this.setState({
|
||||
formData: {},
|
||||
continueTitle: DEFAULT_CONTINUE_TITLE,
|
||||
step: Steps.PasswordStep,
|
||||
title: this.props.changePassword ? 'Change Password' : 'Account Update'
|
||||
title: this.props.changePassword ? 'Change Password' : 'Account Update',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -78,7 +82,7 @@ class PasswordWizardCtrl extends PureViewCtrl<Props, State> implements PasswordW
|
||||
resetContinueState() {
|
||||
this.setState({
|
||||
showSpinner: false,
|
||||
continueTitle: DEFAULT_CONTINUE_TITLE
|
||||
continueTitle: DEFAULT_CONTINUE_TITLE,
|
||||
});
|
||||
this.isContinuing = false;
|
||||
}
|
||||
@@ -95,7 +99,7 @@ class PasswordWizardCtrl extends PureViewCtrl<Props, State> implements PasswordW
|
||||
this.isContinuing = true;
|
||||
await this.setState({
|
||||
showSpinner: true,
|
||||
continueTitle: "Generating Keys..."
|
||||
continueTitle: 'Generating Keys...',
|
||||
});
|
||||
const valid = await this.validateCurrentPassword();
|
||||
if (!valid) {
|
||||
@@ -110,8 +114,8 @@ class PasswordWizardCtrl extends PureViewCtrl<Props, State> implements PasswordW
|
||||
this.isContinuing = false;
|
||||
this.setState({
|
||||
showSpinner: false,
|
||||
continueTitle: "Finish",
|
||||
step: Steps.FinishStep
|
||||
continueTitle: 'Finish',
|
||||
step: Steps.FinishStep,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -119,43 +123,43 @@ class PasswordWizardCtrl extends PureViewCtrl<Props, State> implements PasswordW
|
||||
return this.setState({
|
||||
formData: {
|
||||
...this.state.formData,
|
||||
...formData
|
||||
}
|
||||
...formData,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async validateCurrentPassword() {
|
||||
const currentPassword = this.state.formData.currentPassword;
|
||||
const newPass = this.props.securityUpdate ? currentPassword : this.state.formData.newPassword;
|
||||
const newPass = this.props.securityUpdate
|
||||
? currentPassword
|
||||
: this.state.formData.newPassword;
|
||||
if (!currentPassword || currentPassword.length === 0) {
|
||||
this.application.alertService!.alert(
|
||||
"Please enter your current password."
|
||||
'Please enter your current password.'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if (this.props.changePassword) {
|
||||
if (!newPass || newPass.length === 0) {
|
||||
this.application.alertService!.alert(
|
||||
"Please enter a new password."
|
||||
);
|
||||
this.application.alertService!.alert('Please enter a new password.');
|
||||
return false;
|
||||
}
|
||||
if (newPass !== this.state.formData.newPasswordConfirmation) {
|
||||
this.application.alertService!.alert(
|
||||
"Your new password does not match its confirmation."
|
||||
'Your new password does not match its confirmation.'
|
||||
);
|
||||
this.setFormDataState({
|
||||
status: undefined
|
||||
status: undefined,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (!this.application.getUser()?.email) {
|
||||
this.application.alertService!.alert(
|
||||
"We don't have your email stored. Please log out then log back in to fix this issue."
|
||||
"We don't have your email stored. Please sign out then log back in to fix this issue."
|
||||
);
|
||||
this.setFormDataState({
|
||||
status: undefined
|
||||
status: undefined,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
@@ -166,7 +170,7 @@ class PasswordWizardCtrl extends PureViewCtrl<Props, State> implements PasswordW
|
||||
);
|
||||
if (!success) {
|
||||
this.application.alertService!.alert(
|
||||
"The current password you entered is not correct. Please try again."
|
||||
'The current password you entered is not correct. Please try again.'
|
||||
);
|
||||
}
|
||||
return success;
|
||||
@@ -176,10 +180,10 @@ class PasswordWizardCtrl extends PureViewCtrl<Props, State> implements PasswordW
|
||||
await this.application.downloadBackup();
|
||||
await this.setState({
|
||||
lockContinue: true,
|
||||
processing: true
|
||||
processing: true,
|
||||
});
|
||||
await this.setFormDataState({
|
||||
status: "Processing encryption keys…"
|
||||
status: 'Processing encryption keys…',
|
||||
});
|
||||
const newPassword = this.props.securityUpdate
|
||||
? this.state.formData.currentPassword
|
||||
@@ -195,16 +199,16 @@ class PasswordWizardCtrl extends PureViewCtrl<Props, State> implements PasswordW
|
||||
});
|
||||
if (!success) {
|
||||
this.setFormDataState({
|
||||
status: "Unable to process your password. Please try again."
|
||||
status: 'Unable to process your password. Please try again.',
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
formData: {
|
||||
...this.state.formData,
|
||||
status: this.props.changePassword
|
||||
? "Successfully changed password."
|
||||
: "Successfully performed account update."
|
||||
}
|
||||
? 'Successfully changed password.'
|
||||
: 'Successfully performed account update.',
|
||||
},
|
||||
});
|
||||
}
|
||||
return success;
|
||||
@@ -213,7 +217,7 @@ class PasswordWizardCtrl extends PureViewCtrl<Props, State> implements PasswordW
|
||||
dismiss() {
|
||||
if (this.state.lockContinue) {
|
||||
this.application.alertService!.alert(
|
||||
"Cannot close window until pending tasks are complete."
|
||||
'Cannot close window until pending tasks are complete.'
|
||||
);
|
||||
} else {
|
||||
const elem = this.$element;
|
||||
@@ -234,7 +238,7 @@ export class PasswordWizard extends WebDirective {
|
||||
this.bindToController = true;
|
||||
this.scope = {
|
||||
type: '=',
|
||||
application: '='
|
||||
application: '=',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,7 +102,7 @@ class RevisionPreviewModalCtrl extends PureViewCtrl implements RevisionPreviewSc
|
||||
});
|
||||
} else {
|
||||
this.application.changeAndSaveItem(this.uuid, (mutator) => {
|
||||
mutator.setContent(this.content);
|
||||
mutator.unsafe_setCustomContent(this.content);
|
||||
}, true, PayloadSource.RemoteActionRetrieved);
|
||||
}
|
||||
this.dismiss();
|
||||
|
||||
24
app/assets/javascripts/enums.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
export enum HtmlInputTypes {
|
||||
Button = 'button',
|
||||
Checkbox = 'checkbox',
|
||||
Color = 'color',
|
||||
Date = 'date',
|
||||
DateTimeLocal = 'datetime-local',
|
||||
Email = 'email',
|
||||
File = 'file',
|
||||
Hidden = 'hidden',
|
||||
Image = 'image',
|
||||
Month = 'month',
|
||||
Number = 'number',
|
||||
Password = 'password',
|
||||
Radio = 'radio',
|
||||
Range = 'range',
|
||||
Reset = 'reset',
|
||||
Search = 'search',
|
||||
Submit = 'submit',
|
||||
Tel = 'tel',
|
||||
Text = 'text',
|
||||
Time = 'time',
|
||||
Url = 'url',
|
||||
Week = 'week'
|
||||
}
|
||||
11
app/assets/javascripts/hooks/useBeforeUnload.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import { useEffect } from '@node_modules/preact/hooks';
|
||||
|
||||
export const useBeforeUnload = (): void => {
|
||||
useEffect(() => {
|
||||
window.onbeforeunload = () => true;
|
||||
|
||||
return () => {
|
||||
window.onbeforeunload = null;
|
||||
};
|
||||
}, []);
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
export enum RootScopeMessages {
|
||||
ReloadExtendedData = 'reload-ext-data',
|
||||
NewUpdateAvailable = 'new-update-available'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ const PREFERENCE_IDS = [
|
||||
'account',
|
||||
'appearance',
|
||||
'security',
|
||||
'extensions',
|
||||
'listed',
|
||||
'shortcuts',
|
||||
'accessibility',
|
||||
@@ -28,6 +29,7 @@ const PREFERENCES_MENU_ITEMS: PreferencesMenuItem[] = [
|
||||
{ id: 'account', label: 'Account', icon: 'user' },
|
||||
{ id: 'appearance', label: 'Appearance', icon: 'themes' },
|
||||
{ id: 'security', label: 'Security', icon: 'security' },
|
||||
{ id: 'extensions', label: 'Extensions', icon: 'tune' },
|
||||
{ id: 'listed', label: 'Listed', icon: 'listed' },
|
||||
{ id: 'shortcuts', label: 'Shortcuts', icon: 'keyboard' },
|
||||
{ id: 'accessibility', label: 'Accessibility', icon: 'accessibility' },
|
||||
@@ -65,7 +67,7 @@ export class PreferencesMenu {
|
||||
);
|
||||
}
|
||||
|
||||
selectPane(key: PreferenceId) {
|
||||
selectPane(key: PreferenceId): void {
|
||||
this._selectedPane = key;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { FunctionComponent } from 'preact';
|
||||
import { MenuItem } from './components';
|
||||
import { PreferencesMenu } from './preferences-menu';
|
||||
import { PreferencesMenu } from './PreferencesMenu';
|
||||
|
||||
export const PreferencesMenuView: FunctionComponent<{
|
||||
menu: PreferencesMenu;
|
||||
|
||||
@@ -1,27 +1,58 @@
|
||||
import { RoundIconButton } from '@/components/RoundIconButton';
|
||||
import { TitleBar, Title } from '@/components/TitleBar';
|
||||
import { FunctionComponent } from 'preact';
|
||||
import { AccountPreferences, HelpAndFeedback, Security } from './panes';
|
||||
import {
|
||||
AccountPreferences,
|
||||
HelpAndFeedback,
|
||||
Listed,
|
||||
General,
|
||||
Security,
|
||||
} from './panes';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { PreferencesMenu } from './preferences-menu';
|
||||
import { PreferencesMenu } from './PreferencesMenu';
|
||||
import { PreferencesMenuView } from './PreferencesMenuView';
|
||||
import { WebApplication } from '@/ui_models/application';
|
||||
import { MfaProps } from './panes/two-factor-auth/MfaProps';
|
||||
import { AppState } from '@/ui_models/app_state';
|
||||
import { useEffect, useMemo } from 'preact/hooks';
|
||||
import { Extensions } from './panes/Extensions';
|
||||
|
||||
const PaneSelector: FunctionComponent<{
|
||||
prefs: PreferencesMenu;
|
||||
interface PreferencesProps extends MfaProps {
|
||||
application: WebApplication;
|
||||
}> = observer(({ prefs: menu, application }) => {
|
||||
switch (menu.selectedPaneId) {
|
||||
appState: AppState;
|
||||
closePreferences: () => void;
|
||||
}
|
||||
|
||||
const PaneSelector: FunctionComponent<
|
||||
PreferencesProps & { menu: PreferencesMenu }
|
||||
> = observer((props) => {
|
||||
switch (props.menu.selectedPaneId) {
|
||||
case 'general':
|
||||
return null;
|
||||
return (
|
||||
<General appState={props.appState} application={props.application} />
|
||||
);
|
||||
case 'account':
|
||||
return <AccountPreferences application={application} />;
|
||||
return (
|
||||
<AccountPreferences
|
||||
application={props.application}
|
||||
appState={props.appState}
|
||||
/>
|
||||
);
|
||||
case 'appearance':
|
||||
return null;
|
||||
case 'security':
|
||||
return <Security />;
|
||||
return (
|
||||
<Security
|
||||
mfaProvider={props.mfaProvider}
|
||||
userProvider={props.userProvider}
|
||||
appState={props.appState}
|
||||
application={props.application}
|
||||
/>
|
||||
);
|
||||
case 'extensions':
|
||||
return <Extensions application={props.application} />;
|
||||
case 'listed':
|
||||
return null;
|
||||
return <Listed application={props.application} />;
|
||||
case 'shortcuts':
|
||||
return null;
|
||||
case 'accessibility':
|
||||
@@ -33,55 +64,49 @@ const PaneSelector: FunctionComponent<{
|
||||
}
|
||||
});
|
||||
|
||||
const PreferencesCanvas: FunctionComponent<{
|
||||
preferences: PreferencesMenu;
|
||||
application: WebApplication;
|
||||
}> = observer(({ preferences: prefs, application }) => (
|
||||
const PreferencesCanvas: FunctionComponent<
|
||||
PreferencesProps & { menu: PreferencesMenu }
|
||||
> = observer((props) => (
|
||||
<div className="flex flex-row flex-grow min-h-0 justify-between">
|
||||
<PreferencesMenuView menu={prefs}></PreferencesMenuView>
|
||||
<PaneSelector prefs={prefs} application={application} />
|
||||
<PreferencesMenuView menu={props.menu} />
|
||||
<PaneSelector {...props} />
|
||||
</div>
|
||||
));
|
||||
|
||||
const PreferencesView: FunctionComponent<{
|
||||
close: () => void;
|
||||
application: WebApplication;
|
||||
}> = observer(
|
||||
({ close, application }) => {
|
||||
const prefs = new PreferencesMenu();
|
||||
export const PreferencesView: FunctionComponent<PreferencesProps> = observer(
|
||||
(props) => {
|
||||
const menu = useMemo(() => new PreferencesMenu(), []);
|
||||
|
||||
useEffect(() => {
|
||||
menu.selectPane(props.appState.preferences.currentPane);
|
||||
const removeEscKeyObserver = props.application.io.addKeyObserver({
|
||||
key: 'Escape',
|
||||
onKeyDown: (event) => {
|
||||
event.preventDefault();
|
||||
props.closePreferences();
|
||||
},
|
||||
});
|
||||
return () => {
|
||||
removeEscKeyObserver();
|
||||
};
|
||||
}, [props, menu]);
|
||||
|
||||
return (
|
||||
<div className="sn-full-screen flex flex-col bg-contrast z-index-preferences">
|
||||
<div className="h-full w-full absolute top-left-0 flex flex-col bg-contrast z-index-preferences">
|
||||
<TitleBar className="items-center justify-between">
|
||||
{/* div is added so flex justify-between can center the title */}
|
||||
<div className="h-8 w-8" />
|
||||
<Title className="text-lg">Your preferences for Standard Notes</Title>
|
||||
<RoundIconButton
|
||||
onClick={() => {
|
||||
close();
|
||||
props.closePreferences();
|
||||
}}
|
||||
type="normal"
|
||||
icon="close"
|
||||
/>
|
||||
</TitleBar>
|
||||
<PreferencesCanvas preferences={prefs} application={application} />
|
||||
<PreferencesCanvas {...props} menu={menu} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export interface PreferencesWrapperProps {
|
||||
appState: { preferences: { isOpen: boolean; closePreferences: () => void } };
|
||||
application: WebApplication;
|
||||
}
|
||||
|
||||
export const PreferencesViewWrapper: FunctionComponent<PreferencesWrapperProps> =
|
||||
observer(({ appState, application }) => {
|
||||
if (!appState.preferences.isOpen) return null;
|
||||
return (
|
||||
<PreferencesView
|
||||
application={application}
|
||||
close={() => appState.preferences.closePreferences()}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { FunctionComponent } from 'preact';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { WebApplication } from '@/ui_models/application';
|
||||
import { PreferencesView } from './PreferencesView';
|
||||
import { AppState } from '@/ui_models/app_state';
|
||||
|
||||
export interface PreferencesViewWrapperProps {
|
||||
appState: AppState;
|
||||
application: WebApplication;
|
||||
}
|
||||
|
||||
export const PreferencesViewWrapper: FunctionComponent<PreferencesViewWrapperProps> =
|
||||
observer(({ appState, application }) => {
|
||||
if (!appState.preferences.isOpen) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<PreferencesView
|
||||
closePreferences={() => appState.preferences.closePreferences()}
|
||||
application={application}
|
||||
appState={appState}
|
||||
mfaProvider={application}
|
||||
userProvider={application}
|
||||
/>
|
||||
);
|
||||
});
|
||||
@@ -4,23 +4,25 @@ export const Title: FunctionComponent = ({ children }) => (
|
||||
<h2 className="text-base m-0 mb-1">{children}</h2>
|
||||
);
|
||||
|
||||
export const Subtitle: FunctionComponent = ({ children }) => (
|
||||
<h4 className="font-medium text-sm m-0 mb-1">{children}</h4>
|
||||
export const Subtitle: FunctionComponent<{ className?: string }> = ({ children, className = "" }) => (
|
||||
<h4 className={`font-medium text-sm m-0 mb-1 ${className}`}>{children}</h4>
|
||||
);
|
||||
|
||||
export const Text: FunctionComponent = ({ children }) => (
|
||||
<p className="text-xs">{children}</p>
|
||||
);
|
||||
export const Text: FunctionComponent<{ className?: string }> = ({
|
||||
children,
|
||||
className = '',
|
||||
}) => <p className={`${className} text-xs`}>{children}</p>;
|
||||
|
||||
const buttonClasses = `block bg-default color-text rounded border-solid \
|
||||
border-1 border-gray-300 px-4 py-1.75 font-bold text-sm fit-content mt-3 \
|
||||
focus:bg-contrast hover:bg-contrast `;
|
||||
border-1 border-gray-300 px-4 py-1.75 font-bold text-sm fit-content \
|
||||
focus:bg-contrast hover:bg-contrast border-neutral`;
|
||||
|
||||
export const LinkButton: FunctionComponent<{ label: string; link: string }> = ({
|
||||
label,
|
||||
link,
|
||||
}) => (
|
||||
<a target="_blank" className={buttonClasses} href={link}>
|
||||
export const LinkButton: FunctionComponent<{
|
||||
label: string;
|
||||
link: string;
|
||||
className?: string;
|
||||
}> = ({ label, link, className }) => (
|
||||
<a target="_blank" className={`${className} ${buttonClasses}`} href={link}>
|
||||
{label}
|
||||
</a>
|
||||
);
|
||||
|
||||
@@ -15,13 +15,14 @@ export const MenuItem: FunctionComponent<Props> = ({
|
||||
onClick,
|
||||
}) => (
|
||||
<div
|
||||
className={`preferences-menu-item ${selected ? 'selected' : ''}`}
|
||||
className={`preferences-menu-item select-none ${selected ? 'selected' : ''}`}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
onClick();
|
||||
}}
|
||||
>
|
||||
<Icon className="icon" type={iconType} />
|
||||
<div className="min-w-1" />
|
||||
{label}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
import { FunctionComponent } from 'preact';
|
||||
|
||||
const HorizontalLine: FunctionComponent<{ index: number; length: number }> = ({
|
||||
index,
|
||||
length,
|
||||
}) =>
|
||||
index < length - 1 ? (
|
||||
<hr className="h-1px w-full bg-border no-border" />
|
||||
) : null;
|
||||
|
||||
export const PreferencesSegment: FunctionComponent = ({ children }) => (
|
||||
<div className="flex flex-col">{children}</div>
|
||||
);
|
||||
|
||||
export const PreferencesGroup: FunctionComponent = ({ children }) => (
|
||||
<div className="bg-default border-1 border-solid rounded border-gray-300 px-6 py-6 flex flex-col gap-2">
|
||||
{!Array.isArray(children)
|
||||
? children
|
||||
: children.map((c, i, arr) => (
|
||||
<>
|
||||
{c}
|
||||
<HorizontalLine index={i} length={arr.length} />
|
||||
</>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
export const PreferencesPane: FunctionComponent = ({ children }) => (
|
||||
<div className="preferences-pane flex-grow flex flex-row overflow-y-auto min-h-0">
|
||||
<div className="flex-grow flex flex-col py-6 items-center">
|
||||
<div className="w-125 max-w-125 flex flex-col gap-3">{children}</div>
|
||||
</div>
|
||||
<div className="flex-basis-55 flex-shrink" />
|
||||
</div>
|
||||
);
|
||||
@@ -0,0 +1,24 @@
|
||||
import { FunctionComponent } from 'preact';
|
||||
import { HorizontalSeparator } from '@/components/shared/HorizontalSeparator';
|
||||
|
||||
const HorizontalLine: FunctionComponent<{ index: number; length: number }> = ({
|
||||
index,
|
||||
length,
|
||||
}) => (index < length - 1 ? <HorizontalSeparator classes="my-4" /> : null);
|
||||
|
||||
export const PreferencesGroup: FunctionComponent = ({ children }) => (
|
||||
<div className="bg-default border-1 border-solid rounded border-gray-300 px-6 py-6 flex flex-col mb-3">
|
||||
{Array.isArray(children)
|
||||
? children
|
||||
.filter(
|
||||
(child) => child != undefined && child !== '' && child !== false
|
||||
)
|
||||
.map((child, i, arr) => (
|
||||
<>
|
||||
{child}
|
||||
<HorizontalLine index={i} length={arr.length} />
|
||||
</>
|
||||
))
|
||||
: children}
|
||||
</div>
|
||||
);
|
||||
@@ -0,0 +1,15 @@
|
||||
import { FunctionComponent } from 'preact';
|
||||
|
||||
export const PreferencesPane: FunctionComponent = ({ children }) => (
|
||||
<div className="color-foreground flex-grow flex flex-row overflow-y-auto min-h-0">
|
||||
<div className="flex-grow flex flex-col py-6 items-center">
|
||||
<div className="w-125 max-w-125 flex flex-col">
|
||||
{children != undefined && Array.isArray(children)
|
||||
? children
|
||||
.filter((child) => child != undefined)
|
||||
: children}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-basis-55 flex-shrink" />
|
||||
</div>
|
||||
);
|
||||
@@ -0,0 +1,5 @@
|
||||
import { FunctionComponent } from 'preact';
|
||||
|
||||
export const PreferencesSegment: FunctionComponent = ({ children }) => (
|
||||
<div className="flex flex-col">{children}</div>
|
||||
);
|
||||
@@ -1,3 +1,5 @@
|
||||
export * from './Content';
|
||||
export * from './MenuItem';
|
||||
export * from './Pane';
|
||||
export * from './PreferencesPane';
|
||||
export * from './PreferencesGroup';
|
||||
export * from './PreferencesSegment';
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { toDirective } from '../components/utils';
|
||||
import {
|
||||
PreferencesViewWrapper,
|
||||
PreferencesWrapperProps,
|
||||
} from './PreferencesView';
|
||||
PreferencesViewWrapperProps,
|
||||
} from './PreferencesViewWrapper';
|
||||
|
||||
export const PreferencesDirective = toDirective<PreferencesWrapperProps>(
|
||||
export const PreferencesDirective = toDirective<PreferencesViewWrapperProps>(
|
||||
PreferencesViewWrapper
|
||||
);
|
||||
|
||||
@@ -1,12 +1,39 @@
|
||||
import { Sync } from '@/preferences/panes/account';
|
||||
import {
|
||||
Sync,
|
||||
SubscriptionWrapper,
|
||||
Credentials,
|
||||
SignOutWrapper,
|
||||
Authentication,
|
||||
} from '@/preferences/panes/account';
|
||||
import { PreferencesPane } from '@/preferences/components';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { WebApplication } from '@/ui_models/application';
|
||||
import { AppState } from '@/ui_models/app_state';
|
||||
|
||||
export const AccountPreferences = observer(({application}: {application: WebApplication}) => {
|
||||
return (
|
||||
<PreferencesPane>
|
||||
<Sync application={application} />
|
||||
</PreferencesPane>
|
||||
);
|
||||
});
|
||||
type Props = {
|
||||
application: WebApplication;
|
||||
appState: AppState;
|
||||
};
|
||||
|
||||
export const AccountPreferences = observer(
|
||||
({ application, appState }: Props) => {
|
||||
|
||||
if (!application.hasAccount()) {
|
||||
return (
|
||||
<PreferencesPane>
|
||||
<Authentication application={application} appState={appState} />
|
||||
<SubscriptionWrapper application={application} />
|
||||
<SignOutWrapper application={application} appState={appState} />
|
||||
</PreferencesPane>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<PreferencesPane>
|
||||
<Credentials application={application} />
|
||||
<Sync application={application} />
|
||||
<SignOutWrapper application={application} appState={appState} />
|
||||
</PreferencesPane>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
144
app/assets/javascripts/preferences/panes/Extensions.tsx
Normal file
@@ -0,0 +1,144 @@
|
||||
import { ContentType, SNComponent } from '@standardnotes/snjs';
|
||||
import { Button } from '@/components/Button';
|
||||
import { DecoratedInput } from '@/components/DecoratedInput';
|
||||
import { WebApplication } from '@/ui_models/application';
|
||||
import { FunctionComponent } from 'preact';
|
||||
import {
|
||||
Title,
|
||||
PreferencesGroup,
|
||||
PreferencesPane,
|
||||
PreferencesSegment,
|
||||
} from '../components';
|
||||
import { ConfirmCustomExtension, ExtensionItem } from './extensions-segments';
|
||||
import { useEffect, useRef, useState } from 'preact/hooks';
|
||||
import { FeatureDescription } from '@standardnotes/features';
|
||||
|
||||
const loadExtensions = (application: WebApplication) => application.getItems([
|
||||
ContentType.ActionsExtension,
|
||||
ContentType.Component,
|
||||
ContentType.Theme,
|
||||
]) as SNComponent[];
|
||||
|
||||
function collectFeatures(features: FeatureDescription[] | undefined, versionMap: Map<string, string>) {
|
||||
if (features == undefined) return;
|
||||
for (const feature of features) {
|
||||
versionMap.set(feature.identifier, feature.version);
|
||||
}
|
||||
}
|
||||
|
||||
const loadLatestVersions = (application: WebApplication) => application.getAvailableSubscriptions()
|
||||
.then(subscriptions => {
|
||||
const versionMap: Map<string, string> = new Map();
|
||||
collectFeatures(subscriptions?.CORE_PLAN?.features, versionMap);
|
||||
collectFeatures(subscriptions?.PLUS_PLAN?.features, versionMap);
|
||||
collectFeatures(subscriptions?.PRO_PLAN?.features, versionMap);
|
||||
return versionMap;
|
||||
});
|
||||
|
||||
export const Extensions: FunctionComponent<{
|
||||
application: WebApplication
|
||||
}> = ({ application }) => {
|
||||
|
||||
const [customUrl, setCustomUrl] = useState('');
|
||||
const [confirmableExtension, setConfirmableExtension] = useState<SNComponent | undefined>(undefined);
|
||||
const [extensions, setExtensions] = useState(loadExtensions(application));
|
||||
const [latestVersions, setLatestVersions] = useState<Map<string, string> | undefined>(undefined);
|
||||
|
||||
const confirmableEnd = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (confirmableExtension) {
|
||||
confirmableEnd.current.scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
}, [confirmableExtension, confirmableEnd]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!latestVersions) {
|
||||
loadLatestVersions(application).then(versions => setLatestVersions(versions));
|
||||
}
|
||||
}, [latestVersions, application]);
|
||||
|
||||
const uninstallExtension = async (extension: SNComponent) => {
|
||||
await application.deleteItem(extension);
|
||||
setExtensions(loadExtensions(application));
|
||||
};
|
||||
|
||||
const submitExtensionUrl = async (url: string) => {
|
||||
const component = await application.downloadExternalFeature(url);
|
||||
if (component) {
|
||||
setConfirmableExtension(component);
|
||||
}
|
||||
};
|
||||
|
||||
const handleConfirmExtensionSubmit = async (confirm: boolean) => {
|
||||
if (confirm) {
|
||||
confirmExtension();
|
||||
}
|
||||
setConfirmableExtension(undefined);
|
||||
setCustomUrl('');
|
||||
};
|
||||
|
||||
const confirmExtension = async () => {
|
||||
await application.insertItem(confirmableExtension as SNComponent);
|
||||
setExtensions(loadExtensions(application));
|
||||
};
|
||||
|
||||
const toggleActivateExtension = (extension: SNComponent) => {
|
||||
application.toggleComponent(extension);
|
||||
setExtensions(loadExtensions(application));
|
||||
};
|
||||
|
||||
return (
|
||||
<PreferencesPane>
|
||||
{extensions.length > 0 &&
|
||||
<PreferencesGroup>
|
||||
{
|
||||
extensions
|
||||
.filter(extension => extension.package_info.identifier !== 'org.standardnotes.extensions-manager')
|
||||
.sort((e1, e2) => e1.name.toLowerCase().localeCompare(e2.name.toLowerCase()))
|
||||
.map((extension, i) => (
|
||||
<ExtensionItem
|
||||
application={application}
|
||||
extension={extension}
|
||||
latestVersion={latestVersions?.get(extension.package_info.identifier)}
|
||||
first={i === 0}
|
||||
uninstall={uninstallExtension}
|
||||
toggleActivate={toggleActivateExtension} />
|
||||
))
|
||||
}
|
||||
</PreferencesGroup>
|
||||
}
|
||||
|
||||
<PreferencesGroup>
|
||||
{!confirmableExtension &&
|
||||
<PreferencesSegment>
|
||||
<Title>Install Custom Extension</Title>
|
||||
<div className="min-h-2" />
|
||||
<DecoratedInput
|
||||
placeholder={'Enter Extension URL'}
|
||||
text={customUrl}
|
||||
onChange={(value) => { setCustomUrl(value); }}
|
||||
/>
|
||||
<div className="min-h-2" />
|
||||
<Button
|
||||
className="min-w-20"
|
||||
type="normal"
|
||||
label="Install"
|
||||
onClick={() => submitExtensionUrl(customUrl)}
|
||||
/>
|
||||
|
||||
</PreferencesSegment>
|
||||
}
|
||||
{confirmableExtension &&
|
||||
<PreferencesSegment>
|
||||
<ConfirmCustomExtension
|
||||
component={confirmableExtension}
|
||||
callback={handleConfirmExtensionSubmit}
|
||||
/>
|
||||
<div ref={confirmableEnd} />
|
||||
</PreferencesSegment>
|
||||
}
|
||||
</PreferencesGroup>
|
||||
</PreferencesPane>
|
||||
);
|
||||
};
|
||||
18
app/assets/javascripts/preferences/panes/General.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import { WebApplication } from '@/ui_models/application';
|
||||
import { AppState } from '@/ui_models/app_state';
|
||||
import { FunctionComponent } from 'preact';
|
||||
import { PreferencesPane } from '../components';
|
||||
import { ErrorReporting, Tools, Defaults } from './general-segments';
|
||||
|
||||
interface GeneralProps {
|
||||
appState: AppState;
|
||||
application: WebApplication;
|
||||
}
|
||||
|
||||
export const General: FunctionComponent<GeneralProps> = (props) => (
|
||||
<PreferencesPane>
|
||||
<Tools application={props.application} />
|
||||
<Defaults application={props.application} />
|
||||
<ErrorReporting appState={props.appState} />
|
||||
</PreferencesPane>
|
||||
);
|
||||
@@ -52,7 +52,11 @@ export const HelpAndFeedback: FunctionComponent = () => (
|
||||
</PreferencesSegment>
|
||||
<PreferencesSegment>
|
||||
<Subtitle>Can’t find your question here?</Subtitle>
|
||||
<LinkButton label="Open FAQ" link="https://standardnotes.com/help" />
|
||||
<LinkButton
|
||||
className="mt-3"
|
||||
label="Open FAQ"
|
||||
link="https://standardnotes.com/help"
|
||||
/>
|
||||
</PreferencesSegment>
|
||||
</PreferencesGroup>
|
||||
<PreferencesGroup>
|
||||
@@ -68,6 +72,7 @@ export const HelpAndFeedback: FunctionComponent = () => (
|
||||
before advocating for a feature request.
|
||||
</Text>
|
||||
<LinkButton
|
||||
className="mt-3"
|
||||
label="Go to the forum"
|
||||
link="https://forum.standardnotes.org/"
|
||||
/>
|
||||
@@ -82,6 +87,7 @@ export const HelpAndFeedback: FunctionComponent = () => (
|
||||
group for discussions on security, themes, editors and more.
|
||||
</Text>
|
||||
<LinkButton
|
||||
className="mt-3"
|
||||
link="https://standardnotes.com/slack"
|
||||
label="Join our Slack group"
|
||||
/>
|
||||
@@ -91,9 +97,9 @@ export const HelpAndFeedback: FunctionComponent = () => (
|
||||
<PreferencesSegment>
|
||||
<Title>Account related issue?</Title>
|
||||
<Text>
|
||||
Send an email to help@standardnotes.org and we’ll sort it out.
|
||||
Send an email to help@standardnotes.com and we’ll sort it out.
|
||||
</Text>
|
||||
<LinkButton link="mailto: help@standardnotes.org" label="Email us" />
|
||||
<LinkButton className="mt-3" link="mailto: help@standardnotes.com" label="Email us" />
|
||||
</PreferencesSegment>
|
||||
</PreferencesGroup>
|
||||
</PreferencesPane>
|
||||
|
||||
116
app/assets/javascripts/preferences/panes/Listed.tsx
Normal file
@@ -0,0 +1,116 @@
|
||||
import {
|
||||
PreferencesGroup,
|
||||
PreferencesPane,
|
||||
PreferencesSegment,
|
||||
Title,
|
||||
Subtitle,
|
||||
Text,
|
||||
LinkButton,
|
||||
} from '../components';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { WebApplication } from '@/ui_models/application';
|
||||
import { ContentType, SNComponent } from '@standardnotes/snjs';
|
||||
import { SNItem } from '@standardnotes/snjs/dist/@types/models/core/item';
|
||||
import { useCallback, useEffect, useState } from 'preact/hooks';
|
||||
import { BlogItem } from './listed/BlogItem';
|
||||
|
||||
type Props = {
|
||||
application: WebApplication;
|
||||
};
|
||||
|
||||
export const Listed = observer(({ application }: Props) => {
|
||||
const [items, setItems] = useState<SNComponent[]>([]);
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
|
||||
const reloadItems = useCallback(() => {
|
||||
const components = application
|
||||
.getItems(ContentType.ActionsExtension)
|
||||
.filter(
|
||||
(item) => (item as SNComponent).package_info?.name === 'Listed'
|
||||
) as SNComponent[];
|
||||
setItems(components);
|
||||
}, [application]);
|
||||
|
||||
useEffect(() => {
|
||||
reloadItems();
|
||||
}, [reloadItems]);
|
||||
|
||||
const disconnectListedBlog = (item: SNItem) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
setIsDeleting(true);
|
||||
application
|
||||
.deleteItem(item)
|
||||
.then(() => {
|
||||
reloadItems();
|
||||
setIsDeleting(false);
|
||||
resolve(true);
|
||||
})
|
||||
.catch((err) => {
|
||||
application.alertService.alert(err);
|
||||
setIsDeleting(false);
|
||||
console.error(err);
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<PreferencesPane>
|
||||
{items.length > 0 && (
|
||||
<PreferencesGroup>
|
||||
<PreferencesSegment>
|
||||
<Title>
|
||||
Your {items.length === 1 ? 'Blog' : 'Blogs'} on Listed
|
||||
</Title>
|
||||
<div className="h-2 w-full" />
|
||||
{items.map((item, index, array) => {
|
||||
return (
|
||||
<BlogItem
|
||||
item={item}
|
||||
showSeparator={index !== array.length - 1}
|
||||
disabled={isDeleting}
|
||||
disconnect={disconnectListedBlog}
|
||||
key={item.uuid}
|
||||
application={application}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</PreferencesSegment>
|
||||
</PreferencesGroup>
|
||||
)}
|
||||
<PreferencesGroup>
|
||||
<PreferencesSegment>
|
||||
<Title>About Listed</Title>
|
||||
<div className="h-2 w-full" />
|
||||
<Subtitle>What is Listed?</Subtitle>
|
||||
<Text>
|
||||
Listed is a free blogging platform that allows you to create a
|
||||
public journal published directly from your notes.{' '}
|
||||
<a
|
||||
target="_blank"
|
||||
href="https://listed.to"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
Learn more
|
||||
</a>
|
||||
</Text>
|
||||
</PreferencesSegment>
|
||||
{items.length === 0 ? (
|
||||
<PreferencesSegment>
|
||||
<Subtitle>How to get started?</Subtitle>
|
||||
<Text>
|
||||
First, you’ll need to sign up for Listed. Once you have your
|
||||
Listed account, follow the instructions to connect it with your
|
||||
Standard Notes account.
|
||||
</Text>
|
||||
<LinkButton
|
||||
className="min-w-20 mt-3"
|
||||
link="https://listed.to"
|
||||
label="Get started"
|
||||
/>
|
||||
</PreferencesSegment>
|
||||
) : null}
|
||||
</PreferencesGroup>
|
||||
</PreferencesPane>
|
||||
);
|
||||
});
|
||||
@@ -1,9 +1,25 @@
|
||||
import { WebApplication } from '@/ui_models/application';
|
||||
import { AppState } from '@/ui_models/app_state';
|
||||
import { FunctionComponent } from 'preact';
|
||||
import { PreferencesPane } from '../components';
|
||||
import { Encryption, PasscodeLock, Protections, DataBackups } from './security-segments';
|
||||
import { TwoFactorAuthWrapper } from './two-factor-auth';
|
||||
import { MfaProps } from './two-factor-auth/MfaProps';
|
||||
|
||||
export const Security: FunctionComponent = () => (
|
||||
interface SecurityProps extends MfaProps {
|
||||
appState: AppState;
|
||||
application: WebApplication;
|
||||
}
|
||||
|
||||
export const Security: FunctionComponent<SecurityProps> = (props) => (
|
||||
<PreferencesPane>
|
||||
<TwoFactorAuthWrapper />
|
||||
<Encryption appState={props.appState} />
|
||||
<Protections application={props.application} />
|
||||
<TwoFactorAuthWrapper
|
||||
mfaProvider={props.mfaProvider}
|
||||
userProvider={props.userProvider}
|
||||
/>
|
||||
<PasscodeLock appState={props.appState} application={props.application} />
|
||||
<DataBackups application={props.application} appState={props.appState} />
|
||||
</PreferencesPane>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import { AccountMenuPane } from '@/components/AccountMenu';
|
||||
import { Button } from '@/components/Button';
|
||||
import {
|
||||
PreferencesGroup,
|
||||
PreferencesSegment,
|
||||
Subtitle,
|
||||
Text,
|
||||
Title,
|
||||
} from '@/preferences/components';
|
||||
import { WebApplication } from '@/ui_models/application';
|
||||
import { AppState } from '@/ui_models/app_state';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { FunctionComponent } from 'preact';
|
||||
|
||||
export const Authentication: FunctionComponent<{
|
||||
application: WebApplication;
|
||||
appState: AppState;
|
||||
}> = observer(({ appState }) => {
|
||||
const clickSignIn = () => {
|
||||
appState.preferences.closePreferences();
|
||||
appState.accountMenu.setCurrentPane(AccountMenuPane.SignIn);
|
||||
appState.accountMenu.setShow(true);
|
||||
};
|
||||
|
||||
const clickRegister = () => {
|
||||
appState.preferences.closePreferences();
|
||||
appState.accountMenu.setCurrentPane(AccountMenuPane.Register);
|
||||
appState.accountMenu.setShow(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<PreferencesGroup>
|
||||
<PreferencesSegment>
|
||||
<div className="flex flex-col items-center px-12">
|
||||
<Title>You're not signed in</Title>
|
||||
<Subtitle className="text-center">
|
||||
Sign in to sync your notes and preferences across all your devices
|
||||
and enable end-to-end encryption.
|
||||
</Subtitle>
|
||||
<div className="min-h-3" />
|
||||
<div className="flex flex-row w-full">
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={clickSignIn}
|
||||
label="Sign in"
|
||||
className="flex-grow"
|
||||
/>
|
||||
<div className="min-w-3" />
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={clickRegister}
|
||||
label="Register"
|
||||
className="flex-grow"
|
||||
/>
|
||||
</div>
|
||||
<div className="min-h-3" />
|
||||
<Text className="text-center">
|
||||
Standard Notes is free on every platform, and comes standard with
|
||||
sync and encryption.
|
||||
</Text>
|
||||
</div>
|
||||
</PreferencesSegment>
|
||||
</PreferencesGroup>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,66 @@
|
||||
import { PreferencesGroup, PreferencesSegment, Text, Title } from '@/preferences/components';
|
||||
import { Button } from '@/components/Button';
|
||||
import { WebApplication } from '@/ui_models/application';
|
||||
import { observer } from '@node_modules/mobx-react-lite';
|
||||
import { HorizontalSeparator } from '@/components/shared/HorizontalSeparator';
|
||||
import { dateToLocalizedString } from '@/utils';
|
||||
import { useState } from 'preact/hooks';
|
||||
import { ChangeEmail } from '@/preferences/panes/account/changeEmail';
|
||||
import { PasswordWizardType } from '@/types';
|
||||
import { FunctionComponent } from 'preact';
|
||||
|
||||
type Props = {
|
||||
application: WebApplication;
|
||||
};
|
||||
|
||||
export const Credentials: FunctionComponent<Props> = observer(({ application }: Props) => {
|
||||
const [isChangeEmailDialogOpen, setIsChangeEmailDialogOpen] = useState(false);
|
||||
|
||||
const user = application.getUser();
|
||||
|
||||
const passwordCreatedAtTimestamp = application.getUserPasswordCreationDate() as Date;
|
||||
const passwordCreatedOn = dateToLocalizedString(passwordCreatedAtTimestamp);
|
||||
|
||||
return (
|
||||
<PreferencesGroup>
|
||||
<PreferencesSegment>
|
||||
<Title>Credentials</Title>
|
||||
<div className={'text-input mt-2'}>
|
||||
Email
|
||||
</div>
|
||||
<Text>
|
||||
You're signed in as <span className='font-bold'>{user?.email}</span>
|
||||
</Text>
|
||||
<Button
|
||||
className='min-w-20 mt-3'
|
||||
type='normal'
|
||||
label='Change email'
|
||||
onClick={() => {
|
||||
setIsChangeEmailDialogOpen(true);
|
||||
}}
|
||||
/>
|
||||
<HorizontalSeparator classes='mt-5 mb-3' />
|
||||
<div className={'text-input mt-2'}>
|
||||
Password
|
||||
</div>
|
||||
<Text>
|
||||
Current password was set on <span className='font-bold'>{passwordCreatedOn}</span>
|
||||
</Text>
|
||||
<Button
|
||||
className='min-w-20 mt-3'
|
||||
type='normal'
|
||||
label='Change password'
|
||||
onClick={() => {
|
||||
application.presentPasswordWizard(PasswordWizardType.ChangePassword);
|
||||
}}
|
||||
/>
|
||||
{isChangeEmailDialogOpen && (
|
||||
<ChangeEmail
|
||||
onCloseDialog={() => setIsChangeEmailDialogOpen(false)}
|
||||
application={application}
|
||||
/>
|
||||
)}
|
||||
</PreferencesSegment>
|
||||
</PreferencesGroup>
|
||||
);
|
||||
});
|
||||
104
app/assets/javascripts/preferences/panes/account/SignOutView.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
import { Button } from '@/components/Button';
|
||||
import { ConfirmSignoutContainer } from '@/components/ConfirmSignoutModal';
|
||||
import { OtherSessionsSignOutContainer } from '@/components/OtherSessionsSignOut';
|
||||
import {
|
||||
PreferencesGroup,
|
||||
PreferencesSegment,
|
||||
Subtitle,
|
||||
Text,
|
||||
Title,
|
||||
} from '@/preferences/components';
|
||||
import { WebApplication } from '@/ui_models/application';
|
||||
import { AppState } from '@/ui_models/app_state';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { FunctionComponent } from 'preact';
|
||||
|
||||
const SignOutView: FunctionComponent<{
|
||||
application: WebApplication;
|
||||
appState: AppState;
|
||||
}> = observer(({ application, appState }) => {
|
||||
return (
|
||||
<>
|
||||
<PreferencesGroup>
|
||||
<PreferencesSegment>
|
||||
<Title>Sign out</Title>
|
||||
<div className="min-h-2" />
|
||||
<Subtitle>Other devices</Subtitle>
|
||||
<Text>Want to sign out on all devices except this one?</Text>
|
||||
<div className="min-h-3" />
|
||||
<div className="flex flex-row">
|
||||
<Button
|
||||
className="mr-3"
|
||||
type="normal"
|
||||
label="Sign out other sessions"
|
||||
onClick={() => {
|
||||
appState.accountMenu.setOtherSessionsSignOut(true);
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
type="normal"
|
||||
label="Manage sessions"
|
||||
onClick={() => appState.openSessionsModal()}
|
||||
/>
|
||||
</div>
|
||||
</PreferencesSegment>
|
||||
<PreferencesSegment>
|
||||
<Subtitle>This device</Subtitle>
|
||||
<Text>This will delete all local items and preferences.</Text>
|
||||
<div className="min-h-3" />
|
||||
<Button
|
||||
type="danger"
|
||||
label="Sign out and clear local data"
|
||||
onClick={() => {
|
||||
appState.accountMenu.setSigningOut(true);
|
||||
}}
|
||||
/>
|
||||
</PreferencesSegment>
|
||||
</PreferencesGroup>
|
||||
<OtherSessionsSignOutContainer
|
||||
appState={appState}
|
||||
application={application}
|
||||
/>
|
||||
|
||||
<ConfirmSignoutContainer appState={appState} application={application} />
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
const ClearSessionDataView: FunctionComponent<{
|
||||
application: WebApplication;
|
||||
appState: AppState;
|
||||
}> = observer(({ application, appState }) => {
|
||||
return (
|
||||
<>
|
||||
<PreferencesGroup>
|
||||
<PreferencesSegment>
|
||||
<Title>Clear session data</Title>
|
||||
<div className="min-h-2" />
|
||||
<Text>This will delete all local items and preferences.</Text>
|
||||
<div className="min-h-3" />
|
||||
<Button
|
||||
type="danger"
|
||||
label="Clear Session Data"
|
||||
onClick={() => {
|
||||
appState.accountMenu.setSigningOut(true);
|
||||
}}
|
||||
/>
|
||||
</PreferencesSegment>
|
||||
</PreferencesGroup>
|
||||
|
||||
<ConfirmSignoutContainer appState={appState} application={application} />
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export const SignOutWrapper: FunctionComponent<{
|
||||
application: WebApplication;
|
||||
appState: AppState;
|
||||
}> = observer(({ application, appState }) => {
|
||||
if (!application.hasAccount())
|
||||
return (
|
||||
<ClearSessionDataView appState={appState} application={application} />
|
||||
);
|
||||
return <SignOutView appState={appState} application={application} />;
|
||||
});
|
||||
@@ -1,4 +1,9 @@
|
||||
import { PreferencesGroup, PreferencesSegment, Text, Title } from '@/preferences/components';
|
||||
import {
|
||||
PreferencesGroup,
|
||||
PreferencesSegment,
|
||||
Text,
|
||||
Title,
|
||||
} from '@/preferences/components';
|
||||
import { Button } from '@/components/Button';
|
||||
import { SyncQueueStrategy } from '@node_modules/@standardnotes/snjs';
|
||||
import { STRING_GENERIC_SYNC_ERROR } from '@/strings';
|
||||
@@ -6,55 +11,60 @@ import { useState } from '@node_modules/preact/hooks';
|
||||
import { dateToLocalizedString } from '@/utils';
|
||||
import { observer } from '@node_modules/mobx-react-lite';
|
||||
import { WebApplication } from '@/ui_models/application';
|
||||
import { FunctionComponent } from 'preact';
|
||||
|
||||
type Props = {
|
||||
application: WebApplication;
|
||||
};
|
||||
|
||||
const Sync = observer(({ application }: Props) => {
|
||||
const formatLastSyncDate = (lastUpdatedDate: Date) => {
|
||||
return dateToLocalizedString(lastUpdatedDate);
|
||||
};
|
||||
export const formatLastSyncDate = (lastUpdatedDate: Date) => {
|
||||
return dateToLocalizedString(lastUpdatedDate);
|
||||
};
|
||||
|
||||
const [isSyncingInProgress, setIsSyncingInProgress] = useState(false);
|
||||
const [lastSyncDate, setLastSyncDate] = useState(formatLastSyncDate(application.getLastSyncDate() as Date));
|
||||
export const Sync: FunctionComponent<Props> = observer(
|
||||
({ application }: Props) => {
|
||||
const [isSyncingInProgress, setIsSyncingInProgress] = useState(false);
|
||||
const [lastSyncDate, setLastSyncDate] = useState(
|
||||
formatLastSyncDate(application.getLastSyncDate() as Date)
|
||||
);
|
||||
|
||||
const doSynchronization = async () => {
|
||||
setIsSyncingInProgress(true);
|
||||
const doSynchronization = async () => {
|
||||
setIsSyncingInProgress(true);
|
||||
|
||||
const response = await application.sync({
|
||||
queueStrategy: SyncQueueStrategy.ForceSpawnNew,
|
||||
checkIntegrity: true
|
||||
});
|
||||
setIsSyncingInProgress(false);
|
||||
if (response && response.error) {
|
||||
application.alertService!.alert(STRING_GENERIC_SYNC_ERROR);
|
||||
} else {
|
||||
setLastSyncDate(formatLastSyncDate(application.getLastSyncDate() as Date));
|
||||
}
|
||||
};
|
||||
const response = await application.sync({
|
||||
queueStrategy: SyncQueueStrategy.ForceSpawnNew,
|
||||
checkIntegrity: true,
|
||||
});
|
||||
setIsSyncingInProgress(false);
|
||||
if (response && response.error) {
|
||||
application.alertService!.alert(STRING_GENERIC_SYNC_ERROR);
|
||||
} else {
|
||||
setLastSyncDate(
|
||||
formatLastSyncDate(application.getLastSyncDate() as Date)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<PreferencesGroup>
|
||||
<PreferencesSegment>
|
||||
<div className='flex flex-row items-center'>
|
||||
<div className='flex-grow flex flex-col'>
|
||||
<Title>Sync</Title>
|
||||
<Text>
|
||||
Last synced <span className='font-bold'>on {lastSyncDate}</span>
|
||||
</Text>
|
||||
<Button
|
||||
className='min-w-20 mt-3'
|
||||
type='normal'
|
||||
label='Sync now'
|
||||
disabled={isSyncingInProgress}
|
||||
onClick={doSynchronization}
|
||||
/>
|
||||
return (
|
||||
<PreferencesGroup>
|
||||
<PreferencesSegment>
|
||||
<div className="flex flex-row items-center">
|
||||
<div className="flex-grow flex flex-col">
|
||||
<Title>Sync</Title>
|
||||
<Text>
|
||||
Last synced <span className="font-bold">on {lastSyncDate}</span>
|
||||
</Text>
|
||||
<Button
|
||||
className="min-w-20 mt-3"
|
||||
type="normal"
|
||||
label="Sync now"
|
||||
disabled={isSyncingInProgress}
|
||||
onClick={doSynchronization}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PreferencesSegment>
|
||||
</PreferencesGroup>
|
||||
);
|
||||
});
|
||||
|
||||
export default Sync;
|
||||
</PreferencesSegment>
|
||||
</PreferencesGroup>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { DecoratedInput } from '@/components/DecoratedInput';
|
||||
import { StateUpdater } from 'preact/hooks';
|
||||
import { FunctionalComponent } from 'preact';
|
||||
import { HtmlInputTypes } from '@/enums';
|
||||
|
||||
type Props = {
|
||||
setNewEmail: StateUpdater<string>
|
||||
setCurrentPassword: StateUpdater<string>
|
||||
}
|
||||
export const ChangeEmailForm: FunctionalComponent<Props> = ({
|
||||
setNewEmail,
|
||||
setCurrentPassword
|
||||
}) => {
|
||||
return (
|
||||
(
|
||||
<>
|
||||
<div className={'mt-2 mb-3'}>
|
||||
<DecoratedInput
|
||||
onChange={(newEmail) => {
|
||||
setNewEmail(newEmail);
|
||||
}}
|
||||
placeholder={'New Email'}
|
||||
/>
|
||||
</div>
|
||||
<div className={'mt-2 mb-3'}>
|
||||
<DecoratedInput
|
||||
type={HtmlInputTypes.Password}
|
||||
onChange={(currentPassword) => {
|
||||
setCurrentPassword(currentPassword);
|
||||
}}
|
||||
placeholder={'Current Password'}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
import { FunctionalComponent } from 'preact';
|
||||
|
||||
export const ChangeEmailSuccess: FunctionalComponent = () => {
|
||||
return (
|
||||
<>
|
||||
<div className={'sk-label sk-bold info'}>Your email has been successfully changed.</div>
|
||||
<p className={'sk-p'}>
|
||||
Please ensure you are running the latest version of Standard Notes on all platforms to ensure maximum compatibility.
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,188 @@
|
||||
import { useState } from '@node_modules/preact/hooks';
|
||||
import {
|
||||
ModalDialog,
|
||||
ModalDialogButtons,
|
||||
ModalDialogDescription,
|
||||
ModalDialogLabel,
|
||||
} from '@/components/shared/ModalDialog';
|
||||
import { Button } from '@/components/Button';
|
||||
import { FunctionalComponent } from 'preact';
|
||||
import { WebApplication } from '@/ui_models/application';
|
||||
import { useBeforeUnload } from '@/hooks/useBeforeUnload';
|
||||
import { ChangeEmailForm } from './ChangeEmailForm';
|
||||
import { ChangeEmailSuccess } from './ChangeEmailSuccess';
|
||||
import { isEmailValid } from '@/utils';
|
||||
|
||||
enum SubmitButtonTitles {
|
||||
Default = 'Continue',
|
||||
GeneratingKeys = 'Generating Keys...',
|
||||
Finish = 'Finish',
|
||||
}
|
||||
|
||||
enum Steps {
|
||||
InitialStep,
|
||||
FinishStep,
|
||||
}
|
||||
|
||||
type Props = {
|
||||
onCloseDialog: () => void;
|
||||
application: WebApplication;
|
||||
};
|
||||
|
||||
export const ChangeEmail: FunctionalComponent<Props> = ({
|
||||
onCloseDialog,
|
||||
application,
|
||||
}) => {
|
||||
const [currentPassword, setCurrentPassword] = useState('');
|
||||
const [newEmail, setNewEmail] = useState('');
|
||||
const [isContinuing, setIsContinuing] = useState(false);
|
||||
const [lockContinue, setLockContinue] = useState(false);
|
||||
const [submitButtonTitle, setSubmitButtonTitle] = useState(
|
||||
SubmitButtonTitles.Default
|
||||
);
|
||||
const [currentStep, setCurrentStep] = useState(Steps.InitialStep);
|
||||
|
||||
useBeforeUnload();
|
||||
|
||||
const applicationAlertService = application.alertService;
|
||||
|
||||
const validateCurrentPassword = async () => {
|
||||
if (!currentPassword || currentPassword.length === 0) {
|
||||
applicationAlertService.alert('Please enter your current password.');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
const success = await application.validateAccountPassword(currentPassword);
|
||||
if (!success) {
|
||||
applicationAlertService.alert(
|
||||
'The current password you entered is not correct. Please try again.'
|
||||
);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return success;
|
||||
};
|
||||
|
||||
const validateNewEmail = async () => {
|
||||
if (!isEmailValid(newEmail)) {
|
||||
applicationAlertService.alert(
|
||||
'The email you entered has an invalid format. Please review your input and try again.'
|
||||
);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const resetProgressState = () => {
|
||||
setSubmitButtonTitle(SubmitButtonTitles.Default);
|
||||
setIsContinuing(false);
|
||||
};
|
||||
|
||||
const processEmailChange = async () => {
|
||||
await application.downloadBackup();
|
||||
|
||||
setLockContinue(true);
|
||||
|
||||
const response = await application.changeEmail(newEmail, currentPassword);
|
||||
|
||||
const success = !response.error;
|
||||
|
||||
setLockContinue(false);
|
||||
|
||||
return success;
|
||||
};
|
||||
|
||||
const dismiss = () => {
|
||||
if (lockContinue) {
|
||||
applicationAlertService.alert(
|
||||
'Cannot close window until pending tasks are complete.'
|
||||
);
|
||||
} else {
|
||||
onCloseDialog();
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (lockContinue || isContinuing) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentStep === Steps.FinishStep) {
|
||||
dismiss();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
setIsContinuing(true);
|
||||
setSubmitButtonTitle(SubmitButtonTitles.GeneratingKeys);
|
||||
|
||||
const valid =
|
||||
(await validateCurrentPassword()) && (await validateNewEmail());
|
||||
|
||||
if (!valid) {
|
||||
resetProgressState();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const success = await processEmailChange();
|
||||
if (!success) {
|
||||
resetProgressState();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
setIsContinuing(false);
|
||||
setSubmitButtonTitle(SubmitButtonTitles.Finish);
|
||||
setCurrentStep(Steps.FinishStep);
|
||||
};
|
||||
|
||||
const handleDialogClose = () => {
|
||||
if (lockContinue) {
|
||||
applicationAlertService.alert(
|
||||
'Cannot close window until pending tasks are complete.'
|
||||
);
|
||||
} else {
|
||||
onCloseDialog();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ModalDialog>
|
||||
<ModalDialogLabel closeDialog={handleDialogClose}>
|
||||
Change Email
|
||||
</ModalDialogLabel>
|
||||
<ModalDialogDescription>
|
||||
{currentStep === Steps.InitialStep && (
|
||||
<ChangeEmailForm
|
||||
setNewEmail={setNewEmail}
|
||||
setCurrentPassword={setCurrentPassword}
|
||||
/>
|
||||
)}
|
||||
{currentStep === Steps.FinishStep && <ChangeEmailSuccess />}
|
||||
</ModalDialogDescription>
|
||||
<ModalDialogButtons>
|
||||
{currentStep === Steps.InitialStep && (
|
||||
<Button
|
||||
className="min-w-20"
|
||||
type="normal"
|
||||
label="Cancel"
|
||||
onClick={handleDialogClose}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
className="min-w-20"
|
||||
type="primary"
|
||||
label={submitButtonTitle}
|
||||
onClick={handleSubmit}
|
||||
/>
|
||||
</ModalDialogButtons>
|
||||
</ModalDialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
import { DecoratedInput } from '@/components/DecoratedInput';
|
||||
import { StateUpdater } from 'preact/hooks';
|
||||
import { FunctionalComponent } from 'preact';
|
||||
import { HtmlInputTypes } from '@/enums';
|
||||
|
||||
type Props = {
|
||||
setCurrentPassword: StateUpdater<string>
|
||||
setNewPassword: StateUpdater<string>
|
||||
setNewPasswordConfirmation: StateUpdater<string>
|
||||
}
|
||||
export const ChangePasswordForm: FunctionalComponent<Props> = ({
|
||||
setCurrentPassword,
|
||||
setNewPassword,
|
||||
setNewPasswordConfirmation
|
||||
}) => {
|
||||
return (
|
||||
(
|
||||
<>
|
||||
<div className={'mt-2 mb-3'}>
|
||||
<DecoratedInput
|
||||
type={HtmlInputTypes.Password}
|
||||
onChange={(currentPassword) => {
|
||||
setCurrentPassword(currentPassword);
|
||||
}}
|
||||
placeholder={'Current Password'}
|
||||
/>
|
||||
</div>
|
||||
<div className={'mt-2 mb-3'}>
|
||||
<DecoratedInput
|
||||
type={HtmlInputTypes.Password}
|
||||
placeholder={'New Password'}
|
||||
onChange={newPassword => setNewPassword(newPassword)}
|
||||
/>
|
||||
</div>
|
||||
<div className={'mt-2 mb-3'}>
|
||||
<DecoratedInput
|
||||
type={HtmlInputTypes.Password}
|
||||
placeholder={'Confirm New Password'}
|
||||
onChange={newPasswordConfirmation => setNewPasswordConfirmation(newPasswordConfirmation)}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
import { FunctionalComponent } from 'preact';
|
||||
|
||||
export const ChangePasswordSuccess: FunctionalComponent = () => {
|
||||
return (
|
||||
<>
|
||||
<div className={'sk-label sk-bold info'}>Your password has been successfully changed.</div>
|
||||
<p className={'sk-p'}>
|
||||
Please ensure you are running the latest version of Standard Notes on all platforms to ensure maximum compatibility.
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,189 @@
|
||||
import { useState } from '@node_modules/preact/hooks';
|
||||
import {
|
||||
ModalDialog,
|
||||
ModalDialogButtons,
|
||||
ModalDialogDescription,
|
||||
ModalDialogLabel,
|
||||
} from '@/components/shared/ModalDialog';
|
||||
import { Button } from '@/components/Button';
|
||||
import { FunctionalComponent } from 'preact';
|
||||
import { WebApplication } from '@/ui_models/application';
|
||||
import { ChangePasswordSuccess } from '@/preferences/panes/account/changePassword/ChangePasswordSuccess';
|
||||
import { ChangePasswordForm } from '@/preferences/panes/account/changePassword/ChangePasswordForm';
|
||||
import { useBeforeUnload } from '@/hooks/useBeforeUnload';
|
||||
|
||||
enum SubmitButtonTitles {
|
||||
Default = 'Continue',
|
||||
GeneratingKeys = 'Generating Keys...',
|
||||
Finish = 'Finish',
|
||||
}
|
||||
|
||||
enum Steps {
|
||||
InitialStep,
|
||||
FinishStep,
|
||||
}
|
||||
|
||||
type Props = {
|
||||
onCloseDialog: () => void;
|
||||
application: WebApplication;
|
||||
};
|
||||
|
||||
export const ChangePassword: FunctionalComponent<Props> = ({
|
||||
onCloseDialog,
|
||||
application,
|
||||
}) => {
|
||||
const [currentPassword, setCurrentPassword] = useState('');
|
||||
const [newPassword, setNewPassword] = useState('');
|
||||
const [newPasswordConfirmation, setNewPasswordConfirmation] = useState('');
|
||||
const [isContinuing, setIsContinuing] = useState(false);
|
||||
const [lockContinue, setLockContinue] = useState(false);
|
||||
const [submitButtonTitle, setSubmitButtonTitle] = useState(
|
||||
SubmitButtonTitles.Default
|
||||
);
|
||||
const [currentStep, setCurrentStep] = useState(Steps.InitialStep);
|
||||
|
||||
useBeforeUnload();
|
||||
|
||||
const applicationAlertService = application.alertService;
|
||||
|
||||
const validateCurrentPassword = async () => {
|
||||
if (!currentPassword || currentPassword.length === 0) {
|
||||
applicationAlertService.alert('Please enter your current password.');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!newPassword || newPassword.length === 0) {
|
||||
applicationAlertService.alert('Please enter a new password.');
|
||||
return false;
|
||||
}
|
||||
if (newPassword !== newPasswordConfirmation) {
|
||||
applicationAlertService.alert(
|
||||
'Your new password does not match its confirmation.'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!application.getUser()?.email) {
|
||||
applicationAlertService.alert(
|
||||
"We don't have your email stored. Please sign out then sign back in to fix this issue."
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Validate current password */
|
||||
const success = await application.validateAccountPassword(currentPassword);
|
||||
if (!success) {
|
||||
applicationAlertService.alert(
|
||||
'The current password you entered is not correct. Please try again.'
|
||||
);
|
||||
}
|
||||
return success;
|
||||
};
|
||||
|
||||
const resetProgressState = () => {
|
||||
setSubmitButtonTitle(SubmitButtonTitles.Default);
|
||||
setIsContinuing(false);
|
||||
};
|
||||
|
||||
const processPasswordChange = async () => {
|
||||
await application.downloadBackup();
|
||||
|
||||
setLockContinue(true);
|
||||
|
||||
const response = await application.changePassword(
|
||||
currentPassword,
|
||||
newPassword
|
||||
);
|
||||
|
||||
const success = !response.error;
|
||||
|
||||
setLockContinue(false);
|
||||
|
||||
return success;
|
||||
};
|
||||
|
||||
const dismiss = () => {
|
||||
if (lockContinue) {
|
||||
applicationAlertService.alert(
|
||||
'Cannot close window until pending tasks are complete.'
|
||||
);
|
||||
} else {
|
||||
onCloseDialog();
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (lockContinue || isContinuing) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentStep === Steps.FinishStep) {
|
||||
dismiss();
|
||||
return;
|
||||
}
|
||||
setIsContinuing(true);
|
||||
setSubmitButtonTitle(SubmitButtonTitles.GeneratingKeys);
|
||||
|
||||
const valid = await validateCurrentPassword();
|
||||
|
||||
if (!valid) {
|
||||
resetProgressState();
|
||||
return;
|
||||
}
|
||||
|
||||
const success = await processPasswordChange();
|
||||
if (!success) {
|
||||
resetProgressState();
|
||||
return;
|
||||
}
|
||||
setIsContinuing(false);
|
||||
setSubmitButtonTitle(SubmitButtonTitles.Finish);
|
||||
setCurrentStep(Steps.FinishStep);
|
||||
};
|
||||
|
||||
const handleDialogClose = () => {
|
||||
if (lockContinue) {
|
||||
applicationAlertService.alert(
|
||||
'Cannot close window until pending tasks are complete.'
|
||||
);
|
||||
} else {
|
||||
onCloseDialog();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ModalDialog>
|
||||
<ModalDialogLabel closeDialog={handleDialogClose}>
|
||||
Change Password
|
||||
</ModalDialogLabel>
|
||||
<ModalDialogDescription>
|
||||
{currentStep === Steps.InitialStep && (
|
||||
<ChangePasswordForm
|
||||
setCurrentPassword={setCurrentPassword}
|
||||
setNewPassword={setNewPassword}
|
||||
setNewPasswordConfirmation={setNewPasswordConfirmation}
|
||||
/>
|
||||
)}
|
||||
{currentStep === Steps.FinishStep && <ChangePasswordSuccess />}
|
||||
</ModalDialogDescription>
|
||||
<ModalDialogButtons>
|
||||
{currentStep === Steps.InitialStep && (
|
||||
<Button
|
||||
className="min-w-20"
|
||||
type="normal"
|
||||
label="Cancel"
|
||||
onClick={handleDialogClose}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
className="min-w-20"
|
||||
type="primary"
|
||||
label={submitButtonTitle}
|
||||
onClick={handleSubmit}
|
||||
/>
|
||||
</ModalDialogButtons>
|
||||
</ModalDialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1 +1,5 @@
|
||||
export { default as Sync } from './Sync';
|
||||
export { SubscriptionWrapper } from './subscription/SubscriptionWrapper';
|
||||
export { Sync } from './Sync';
|
||||
export { Credentials } from './Credentials';
|
||||
export { SignOutWrapper } from './SignOutView';
|
||||
export { Authentication } from './Authentication';
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import { FunctionalComponent } from "preact";
|
||||
import { LinkButton, Text } from '@/preferences/components';
|
||||
import { Button } from '@/components/Button';
|
||||
import { WebApplication } from "@/ui_models/application";
|
||||
import { useState } from "preact/hooks";
|
||||
import { isDesktopApplication } from "@/utils";
|
||||
|
||||
export const NoSubscription: FunctionalComponent<{
|
||||
application: WebApplication;
|
||||
}> = ({ application }) => {
|
||||
const [isLoadingPurchaseFlow, setIsLoadingPurchaseFlow] = useState(false);
|
||||
const [purchaseFlowError, setPurchaseFlowError] = useState<string | undefined>(undefined);
|
||||
|
||||
const onPurchaseClick = async () => {
|
||||
const errorMessage = 'There was an error when attempting to redirect you to the subscription page.';
|
||||
setIsLoadingPurchaseFlow(true);
|
||||
try {
|
||||
const url = await application.getPurchaseFlowUrl();
|
||||
if (url) {
|
||||
const currentUrl = window.location.href;
|
||||
const successUrl = isDesktopApplication() ? `standardnotes://${currentUrl}` : currentUrl;
|
||||
window.location.assign(`${url}&success_url=${successUrl}`);
|
||||
} else {
|
||||
setPurchaseFlowError(errorMessage);
|
||||
}
|
||||
} catch (e) {
|
||||
setPurchaseFlowError(errorMessage);
|
||||
} finally {
|
||||
setIsLoadingPurchaseFlow(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Text>You don't have a Standard Notes subscription yet.</Text>
|
||||
{isLoadingPurchaseFlow && (
|
||||
<Text>
|
||||
Redirecting you to the subscription page...
|
||||
</Text>
|
||||
)}
|
||||
{purchaseFlowError && (
|
||||
<Text className="color-danger">
|
||||
{purchaseFlowError}
|
||||
</Text>
|
||||
)}
|
||||
<div className="flex">
|
||||
<LinkButton
|
||||
className="min-w-20 mt-3 mr-3"
|
||||
label="Learn More"
|
||||
link="https://standardnotes.com/plans"
|
||||
/>
|
||||
{application.hasAccount() &&
|
||||
<Button
|
||||
className="min-w-20 mt-3"
|
||||
type="primary"
|
||||
label="Subscribe"
|
||||
onClick={onPurchaseClick}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||