diff --git a/config/watcher.ts b/config/watcher.ts index 04c4f5f7d..10dd6e54d 100644 --- a/config/watcher.ts +++ b/config/watcher.ts @@ -19,6 +19,9 @@ const config: Config = { "core/client/stream/**/*.ts", "core/client/stream/**/*.tsx", "core/client/stream/**/*.graphql", + "core/client/auth/**/*.ts", + "core/client/auth/**/*.tsx", + "core/client/auth/**/*.graphql", "core/server/**/*.graphql", ], ignore: [ diff --git a/package-lock.json b/package-lock.json index a08665dc8..a86945926 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5681,6 +5681,12 @@ "ylru": "^1.2.0" } }, + "cached-iterable": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/cached-iterable/-/cached-iterable-0.2.1.tgz", + "integrity": "sha512-8zAVjMjdn/S/QXJaOnqsko0+ZJzXT2Dum2u9TMGg5YR9fxONPrUjuO9VYqnb1AoldXeYVAcNJLgT5Q8WaIJSgA==", + "dev": true + }, "call-me-maybe": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.1.tgz", @@ -9943,9 +9949,9 @@ "integrity": "sha1-A5/fI/iCPkQMOPMnfm/vEXQhWzA=" }, "fluent": { - "version": "0.6.4", - "resolved": "https://registry.npmjs.org/fluent/-/fluent-0.6.4.tgz", - "integrity": "sha512-EXfMJmnGbUgaIC1myIzDk5akAF6+1JrI7KVnNCba2ou7WCKc/2CWa8QshfhImVtettOvEs0z0UVdMrS6zX7pxA==", + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/fluent/-/fluent-0.8.0.tgz", + "integrity": "sha512-bZfthhubEH1lKgGIi0fIDeNkZrfEOu3MrLbi284LdxNG+9Q5gq2KsuoocumqNPStVtWo3S3/1p8RIqd34u3Mzw==", "dev": true }, "fluent-intl-polyfill": { @@ -9964,15 +9970,22 @@ "dev": true }, "fluent-react": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/fluent-react/-/fluent-react-0.7.0.tgz", - "integrity": "sha512-XgAG06hcVW6oQu3NqLB4KACFBDC9broXG4XDP2xqmj+/DPmZlhHMMD73tFz1mBxCs1pLeojmsYdgyl8l6fF4SA==", + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/fluent-react/-/fluent-react-0.8.0.tgz", + "integrity": "sha512-mO8iPb+muWopEDcbNgb66kr6Tl9tPjRpguDaez9W0xU1hiJ9fQ6EfdCxCUQOYsnVnbruLJqxaRsExSRrCamLCg==", "dev": true, "requires": { - "fluent": "^0.4.0 || ^0.6.0", + "cached-iterable": "^0.2.1", + "fluent-sequence": "^0.2.0", "prop-types": "^15.6.0" } }, + "fluent-sequence": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/fluent-sequence/-/fluent-sequence-0.2.0.tgz", + "integrity": "sha512-t8fc4rHvzO9Yk8otP8LkTqWo6mzjdemooQcnHlDrNzrYAnwsSbVFIlthnkzK2pLsRnO9Ybmw4lXOYUx+fAIyJw==", + "dev": true + }, "flush-write-stream": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.0.3.tgz", @@ -10167,24 +10180,24 @@ "dependencies": { "abbrev": { "version": "1.1.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", "optional": true }, "ansi-regex": { "version": "2.1.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" }, "aproba": { "version": "1.2.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", "optional": true }, "are-we-there-yet": { "version": "1.1.4", - "resolved": false, + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.4.tgz", "integrity": "sha1-u13KOCu5TwXhUZQ3PRb9O6HKEQ0=", "optional": true, "requires": { @@ -10194,12 +10207,12 @@ }, "balanced-match": { "version": "1.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" }, "brace-expansion": { "version": "1.1.11", - "resolved": false, + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "requires": { "balanced-match": "^1.0.0", @@ -10208,34 +10221,34 @@ }, "chownr": { "version": "1.0.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.0.1.tgz", "integrity": "sha1-4qdQQqlVGQi+vSW4Uj1fl2nXkYE=", "optional": true }, "code-point-at": { "version": "1.1.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" }, "concat-map": { "version": "0.0.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, "console-control-strings": { "version": "1.1.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" }, "core-util-is": { "version": "1.0.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", "optional": true }, "debug": { "version": "2.6.9", - "resolved": false, + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "optional": true, "requires": { @@ -10244,25 +10257,25 @@ }, "deep-extend": { "version": "0.5.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.5.1.tgz", "integrity": "sha512-N8vBdOa+DF7zkRrDCsaOXoCs/E2fJfx9B9MrKnnSiHNh4ws7eSys6YQE4KvT1cecKmOASYQBhbKjeuDD9lT81w==", "optional": true }, "delegates": { "version": "1.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", "optional": true }, "detect-libc": { "version": "1.0.3", - "resolved": false, + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=", "optional": true }, "fs-minipass": { "version": "1.2.5", - "resolved": false, + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.5.tgz", "integrity": "sha512-JhBl0skXjUPCFH7x6x61gQxrKyXsxB5gcgePLZCwfyCGGsTISMoIeObbrvVeP6Xmyaudw4TT43qV2Gz+iyd2oQ==", "optional": true, "requires": { @@ -10271,13 +10284,13 @@ }, "fs.realpath": { "version": "1.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", "optional": true }, "gauge": { "version": "2.7.4", - "resolved": false, + "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", "optional": true, "requires": { @@ -10293,7 +10306,7 @@ }, "glob": { "version": "7.1.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", "optional": true, "requires": { @@ -10307,13 +10320,13 @@ }, "has-unicode": { "version": "2.0.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", "optional": true }, "iconv-lite": { "version": "0.4.21", - "resolved": false, + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.21.tgz", "integrity": "sha512-En5V9za5mBt2oUA03WGD3TwDv0MKAruqsuxstbMUZaj9W9k/m1CV/9py3l0L5kw9Bln8fdHQmzHSYtvpvTLpKw==", "optional": true, "requires": { @@ -10322,7 +10335,7 @@ }, "ignore-walk": { "version": "3.0.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.1.tgz", "integrity": "sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ==", "optional": true, "requires": { @@ -10331,7 +10344,7 @@ }, "inflight": { "version": "1.0.6", - "resolved": false, + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", "optional": true, "requires": { @@ -10341,18 +10354,18 @@ }, "inherits": { "version": "2.0.3", - "resolved": false, + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" }, "ini": { "version": "1.3.5", - "resolved": false, + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", "optional": true }, "is-fullwidth-code-point": { "version": "1.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", "requires": { "number-is-nan": "^1.0.0" @@ -10360,13 +10373,13 @@ }, "isarray": { "version": "1.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", "optional": true }, "minimatch": { "version": "3.0.4", - "resolved": false, + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", "requires": { "brace-expansion": "^1.1.7" @@ -10374,12 +10387,12 @@ }, "minimist": { "version": "0.0.8", - "resolved": false, + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" }, "minipass": { "version": "2.2.4", - "resolved": false, + "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.2.4.tgz", "integrity": "sha512-hzXIWWet/BzWhYs2b+u7dRHlruXhwdgvlTMDKC6Cb1U7ps6Ac6yQlR39xsbjWJE377YTCtKwIXIpJ5oP+j5y8g==", "requires": { "safe-buffer": "^5.1.1", @@ -10388,7 +10401,7 @@ }, "minizlib": { "version": "1.1.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.1.0.tgz", "integrity": "sha512-4T6Ur/GctZ27nHfpt9THOdRZNgyJ9FZchYO1ceg5S8Q3DNLCKYy44nCZzgCJgcvx2UM8czmqak5BCxJMrq37lA==", "optional": true, "requires": { @@ -10397,7 +10410,7 @@ }, "mkdirp": { "version": "0.5.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", "requires": { "minimist": "0.0.8" @@ -10405,13 +10418,13 @@ }, "ms": { "version": "2.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", "optional": true }, "needle": { "version": "2.2.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/needle/-/needle-2.2.0.tgz", "integrity": "sha512-eFagy6c+TYayorXw/qtAdSvaUpEbBsDwDyxYFgLZ0lTojfH7K+OdBqAF7TAFwDokJaGpubpSGG0wO3iC0XPi8w==", "optional": true, "requires": { @@ -10422,7 +10435,7 @@ }, "node-pre-gyp": { "version": "0.10.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.10.0.tgz", "integrity": "sha512-G7kEonQLRbcA/mOoFoxvlMrw6Q6dPf92+t/l0DFSMuSlDoWaI9JWIyPwK0jyE1bph//CUEL65/Fz1m2vJbmjQQ==", "optional": true, "requires": { @@ -10440,7 +10453,7 @@ }, "nopt": { "version": "4.0.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz", "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", "optional": true, "requires": { @@ -10450,13 +10463,13 @@ }, "npm-bundled": { "version": "1.0.3", - "resolved": false, + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.0.3.tgz", "integrity": "sha512-ByQ3oJ/5ETLyglU2+8dBObvhfWXX8dtPZDMePCahptliFX2iIuhyEszyFk401PZUNQH20vvdW5MLjJxkwU80Ow==", "optional": true }, "npm-packlist": { "version": "1.1.10", - "resolved": false, + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.1.10.tgz", "integrity": "sha512-AQC0Dyhzn4EiYEfIUjCdMl0JJ61I2ER9ukf/sLxJUcZHfo+VyEfz2rMJgLZSS1v30OxPQe1cN0LZA1xbcaVfWA==", "optional": true, "requires": { @@ -10466,7 +10479,7 @@ }, "npmlog": { "version": "4.1.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", "optional": true, "requires": { @@ -10478,18 +10491,18 @@ }, "number-is-nan": { "version": "1.0.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" }, "object-assign": { "version": "4.1.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", "optional": true }, "once": { "version": "1.4.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", "requires": { "wrappy": "1" @@ -10497,19 +10510,19 @@ }, "os-homedir": { "version": "1.0.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", "optional": true }, "os-tmpdir": { "version": "1.0.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", "optional": true }, "osenv": { "version": "0.1.5", - "resolved": false, + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", "optional": true, "requires": { @@ -10519,19 +10532,19 @@ }, "path-is-absolute": { "version": "1.0.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", "optional": true }, "process-nextick-args": { "version": "2.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", "optional": true }, "rc": { "version": "1.2.7", - "resolved": false, + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.7.tgz", "integrity": "sha512-LdLD8xD4zzLsAT5xyushXDNscEjB7+2ulnl8+r1pnESlYtlJtVSoCMBGr30eDRJ3+2Gq89jK9P9e4tCEH1+ywA==", "optional": true, "requires": { @@ -10543,7 +10556,7 @@ "dependencies": { "minimist": { "version": "1.2.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", "optional": true } @@ -10551,7 +10564,7 @@ }, "readable-stream": { "version": "2.3.6", - "resolved": false, + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "optional": true, "requires": { @@ -10566,7 +10579,7 @@ }, "rimraf": { "version": "2.6.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", "optional": true, "requires": { @@ -10575,42 +10588,42 @@ }, "safe-buffer": { "version": "5.1.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" }, "safer-buffer": { "version": "2.1.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "optional": true }, "sax": { "version": "1.2.4", - "resolved": false, + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", "optional": true }, "semver": { "version": "5.5.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==", "optional": true }, "set-blocking": { "version": "2.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", "optional": true }, "signal-exit": { "version": "3.0.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", "optional": true }, "string-width": { "version": "1.0.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", "requires": { "code-point-at": "^1.0.0", @@ -10620,7 +10633,7 @@ }, "string_decoder": { "version": "1.1.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "optional": true, "requires": { @@ -10629,7 +10642,7 @@ }, "strip-ansi": { "version": "3.0.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "requires": { "ansi-regex": "^2.0.0" @@ -10637,13 +10650,13 @@ }, "strip-json-comments": { "version": "2.0.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", "optional": true }, "tar": { "version": "4.4.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.1.tgz", "integrity": "sha512-O+v1r9yN4tOsvl90p5HAP4AEqbYhx4036AGMm075fH9F8Qwi3oJ+v4u50FkT/KkvywNGtwkk0zRI+8eYm1X/xg==", "optional": true, "requires": { @@ -10658,13 +10671,13 @@ }, "util-deprecate": { "version": "1.0.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", "optional": true }, "wide-align": { "version": "1.1.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.2.tgz", "integrity": "sha512-ijDLlyQ7s6x1JgCLur53osjm/UXUYD9+0PbYKrBsYisYXzCxN+HC3mYDNy/dWdmf3AwqwU3CXwDCvsNgGK1S0w==", "optional": true, "requires": { @@ -10673,12 +10686,12 @@ }, "wrappy": { "version": "1.0.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, "yallist": { "version": "3.0.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.2.tgz", "integrity": "sha1-hFK0u36Dx8GI2AQcGoN8dz1ti7k=" } } diff --git a/package.json b/package.json index 5c2f34d7d..32146395e 100644 --- a/package.json +++ b/package.json @@ -151,10 +151,10 @@ "eventemitter2": "^5.0.1", "final-form": "^4.8.1", "flat": "^4.1.0", - "fluent": "^0.6.4", + "fluent": "^0.8.0", "fluent-intl-polyfill": "^0.1.0", "fluent-langneg": "^0.1.0", - "fluent-react": "^0.7.0", + "fluent-react": "^0.8.0", "graphql-playground-middleware-express": "^1.7.2", "graphql-schema-typescript": "^1.2.1", "gulp": "^4.0.0", diff --git a/src/core/build/paths.ts b/src/core/build/paths.ts index a0e495514..bebff0969 100644 --- a/src/core/build/paths.ts +++ b/src/core/build/paths.ts @@ -20,12 +20,15 @@ export default { appLocales: resolveSrc("locales"), appThemeVariables: resolveSrc("core/client/ui/theme/variables.ts"), appThemeVariablesCSS: resolveSrc("core/client/ui/theme/variables.css"), + appStreamHTML: resolveSrc("core/client/stream/index.html"), appStreamLocalesTemplate: resolveSrc("core/client/stream/locales.ts"), appStreamIndex: resolveSrc("core/client/stream/index.tsx"), + appAuthHTML: resolveSrc("core/client/auth/index.html"), appAuthLocalesTemplate: resolveSrc("core/client/auth/locales.ts"), appAuthIndex: resolveSrc("core/client/auth/index.tsx"), + appEmbedIndex: resolveSrc("core/client/embed/index.ts"), appEmbedHTML: resolveSrc("core/client/embed/index.html"), diff --git a/src/core/client/auth/components/App.css b/src/core/client/auth/components/App.css index 3f61a7cae..c36dceb31 100644 --- a/src/core/client/auth/components/App.css +++ b/src/core/client/auth/components/App.css @@ -1,7 +1,7 @@ /* Here we add global stylings for body and document */ :global { body { - margin: "0"; + margin: 0; /* Support for all WebKit browsers. */ -webkit-font-smoothing: antialiased; @@ -12,4 +12,7 @@ .root { width: 100%; + padding: var(--spacing-unit) var(--spacing-unit) calc(2 * var(--spacing-unit)) + var(--spacing-unit); + box-sizing: border-box; } diff --git a/src/core/client/auth/components/App.spec.tsx b/src/core/client/auth/components/App.spec.tsx new file mode 100644 index 000000000..ec7dd130d --- /dev/null +++ b/src/core/client/auth/components/App.spec.tsx @@ -0,0 +1,14 @@ +import { shallow } from "enzyme"; +import React from "react"; + +import { PropTypesOf } from "talk-framework/types"; + +import App from "./App"; + +it("renders sign in", () => { + const props: PropTypesOf = { + view: "SIGN_IN", + }; + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); +}); diff --git a/src/core/client/auth/components/App.tsx b/src/core/client/auth/components/App.tsx index 368c6d716..103bf95dc 100644 --- a/src/core/client/auth/components/App.tsx +++ b/src/core/client/auth/components/App.tsx @@ -1,22 +1,41 @@ -import * as React from "react"; -import { StatelessComponent } from "react"; - -import { Flex } from "talk-ui/components"; - +import React, { StatelessComponent } from "react"; import * as styles from "./App.css"; +import ForgotPasswordContainer from "../containers/ForgotPasswordContainer"; +import ResetPasswordContainer from "../containers/ResetPasswordContainer"; +import SignInContainer from "../containers/SignInContainer"; +import SignUpContainer from "../containers/SignUpContainer"; + +// TODO: (cvle) Remove %future added value when we have Relay 1.6 +// https://github.com/facebook/relay/commit/1e87e43add7667a494f7ff4cfa7f03f1ab8d81a2 +export type View = + | "SIGN_UP" + | "SIGN_IN" + | "FORGOT_PASSWORD" + | "RESET_PASSWORD" + | "%future added value"; + export interface AppProps { - // TODO: (cvle) Remove %future added value when we have Relay 1.6 - // https://github.com/facebook/relay/commit/1e87e43add7667a494f7ff4cfa7f03f1ab8d81a2 - view: "SIGN_UP" | "SIGN_IN" | "FORGOT_PASSWORD" | "%future added value"; + view: View; } -const App: StatelessComponent = props => { - return ( - - Current View: {props.view} - - ); +const renderView = (view: View) => { + switch (view) { + case "SIGN_UP": + return ; + case "SIGN_IN": + return ; + case "FORGOT_PASSWORD": + return ; + case "RESET_PASSWORD": + return ; + default: + throw new Error(`Unknown view ${view}`); + } }; +const App: StatelessComponent = ({ view }) => ( +
{renderView(view)}
+); + export default App; diff --git a/src/core/client/auth/components/ForgotPassword.tsx b/src/core/client/auth/components/ForgotPassword.tsx new file mode 100644 index 000000000..53a13b76a --- /dev/null +++ b/src/core/client/auth/components/ForgotPassword.tsx @@ -0,0 +1,110 @@ +import { Localized } from "fluent-react/compat"; +import React, { StatelessComponent } from "react"; +import { Field, Form } from "react-final-form"; +import { OnSubmit } from "talk-framework/lib/form"; + +import { + composeValidators, + required, + validateEmail, +} from "talk-framework/lib/validation"; + +import { + Button, + CallOut, + FormField, + HorizontalGutter, + InputLabel, + TextField, + Typography, + ValidationMessage, +} from "talk-ui/components"; + +import AutoHeightContainer from "../containers/AutoHeightContainer"; + +interface FormProps { + email: string; +} + +export interface ForgotPasswordForm { + onSubmit: OnSubmit; +} + +const ForgotPassword: StatelessComponent = props => { + return ( +
+ {({ handleSubmit, submitting, submitError }) => ( + + + + + + Forgot Password + + + + + Enter your email address below and we will send you a link to + reset your password. + + + {submitError && ( + + {submitError} + + )} + + {({ input, meta }) => ( + + + Email Address + + + + + {meta.touched && + (meta.error || meta.submitError) && ( + + {meta.error || meta.submitError} + + )} + + )} + + + + + + + )} + + ); +}; + +export default ForgotPassword; diff --git a/src/core/client/auth/components/ResetPassword.tsx b/src/core/client/auth/components/ResetPassword.tsx new file mode 100644 index 000000000..fd75246fb --- /dev/null +++ b/src/core/client/auth/components/ResetPassword.tsx @@ -0,0 +1,154 @@ +import { Localized } from "fluent-react/compat"; +import React, { StatelessComponent } from "react"; +import { Field, Form } from "react-final-form"; +import { OnSubmit } from "talk-framework/lib/form"; + +import { + composeValidators, + required, + validateEqualPasswords, + validatePassword, +} from "talk-framework/lib/validation"; + +import { + Button, + CallOut, + FormField, + HorizontalGutter, + InputDescription, + InputLabel, + TextField, + Typography, + ValidationMessage, +} from "talk-ui/components"; + +import AutoHeightContainer from "../containers/AutoHeightContainer"; + +interface FormProps { + password: string; + confirmPassword: string; +} + +export interface ResetPasswordForm { + onSubmit: OnSubmit; +} + +const ResetPassword: StatelessComponent = props => { + return ( +
+ {({ handleSubmit, submitting, submitError }) => ( + + + + + + Reset Password + + + {submitError && ( + + {submitError} + + )} + + {({ input, meta }) => ( + + + Password + + + + {"Must be at least {$minLength} characters"} + + + + + + {meta.touched && + (meta.error || meta.submitError) && ( + + {meta.error || meta.submitError} + + )} + + )} + + + + {({ input, meta }) => ( + + + Confirm Password + + + + + {meta.touched && + (meta.error || meta.submitError) && ( + + {meta.error || meta.submitError} + + )} + + )} + + + + + + + )} + + ); +}; + +export default ResetPassword; diff --git a/src/core/client/auth/components/SignIn.tsx b/src/core/client/auth/components/SignIn.tsx new file mode 100644 index 000000000..fb973eba3 --- /dev/null +++ b/src/core/client/auth/components/SignIn.tsx @@ -0,0 +1,179 @@ +import { Localized } from "fluent-react/compat"; +import React, { StatelessComponent } from "react"; +import { Field, Form } from "react-final-form"; +import { OnSubmit } from "talk-framework/lib/form"; + +import { + composeValidators, + required, + validateEmail, +} from "talk-framework/lib/validation"; + +import { + Button, + CallOut, + Flex, + FormField, + HorizontalGutter, + InputLabel, + TextField, + Typography, + ValidationMessage, +} from "talk-ui/components"; + +import AutoHeightContainer from "../containers/AutoHeightContainer"; + +interface FormProps { + email: string; + password: string; +} + +export interface SignInForm { + onSubmit: OnSubmit; + onGotoSignUp: () => void; + onGotoForgotPassword: () => void; +} + +const SignIn: StatelessComponent = props => { + return ( +
+ {({ handleSubmit, submitting, submitError }) => ( + + + + + + Sign in to join the conversation + + + + {submitError && ( + + {submitError} + + )} + + + {({ input, meta }) => ( + + + Email Address + + + + + {meta.touched && + (meta.error || meta.submitError) && ( + + {meta.error || meta.submitError} + + )} + + )} + + + + {({ input, meta }) => ( + + + Password + + + + + {meta.touched && + (meta.error || meta.submitError) && ( + + {meta.error || meta.submitError} + + )} + + + + + + + )} + + + + + + + } + > + + {"Don't have an account? "} + + + + + + )} + + ); +}; + +export default SignIn; diff --git a/src/core/client/auth/components/SignUp.tsx b/src/core/client/auth/components/SignUp.tsx new file mode 100644 index 000000000..eee0abd6f --- /dev/null +++ b/src/core/client/auth/components/SignUp.tsx @@ -0,0 +1,253 @@ +import { Localized } from "fluent-react/compat"; +import * as React from "react"; +import { StatelessComponent } from "react"; +import { Field, Form } from "react-final-form"; +import { OnSubmit } from "talk-framework/lib/form"; +import { + composeValidators, + required, + validateEmail, + validateEqualPasswords, + validatePassword, + validateUsername, +} from "talk-framework/lib/validation"; +import { + Button, + CallOut, + Flex, + FormField, + HorizontalGutter, + InputDescription, + InputLabel, + TextField, + Typography, + ValidationMessage, +} from "talk-ui/components"; + +import AutoHeightContainer from "../containers/AutoHeightContainer"; + +interface FormProps { + email: string; + username: string; + password: string; + confirmPassword: string; +} + +export interface SignUpForm { + onSubmit: OnSubmit; + onGotoSignIn: () => void; +} + +const SignUp: StatelessComponent = props => { + return ( +
+ {({ handleSubmit, submitting, submitError }) => ( + + + + + + Sign up to join the conversation + + + {submitError && ( + + {submitError} + + )} + + {({ input, meta }) => ( + + + Email Address + + + + + {meta.touched && + (meta.error || meta.submitError) && ( + + {meta.error || meta.submitError} + + )} + + )} + + + {({ input, meta }) => ( + + + Username + + + + A unique identifier displayed on your comments. You may + use “_” and “.” + + + + + + {meta.touched && + (meta.error || meta.submitError) && ( + + {meta.error || meta.submitError} + + )} + + )} + + + {({ input, meta }) => ( + + + Password + + + + {"Must be at least {$minLength} characters"} + + + + + + {meta.touched && + (meta.error || meta.submitError) && ( + + {meta.error || meta.submitError} + + )} + + )} + + + {({ input, meta }) => ( + + + Confirm Password + + + + + {meta.touched && + (meta.error || meta.submitError) && ( + + {meta.error || meta.submitError} + + )} + + )} + + + + + + + } + > + + {"Already have an account? "} + + + + + + )} + + ); +}; + +export default SignUp; diff --git a/src/core/client/auth/components/__snapshots__/App.spec.tsx.snap b/src/core/client/auth/components/__snapshots__/App.spec.tsx.snap new file mode 100644 index 000000000..449ec1ce6 --- /dev/null +++ b/src/core/client/auth/components/__snapshots__/App.spec.tsx.snap @@ -0,0 +1,9 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders sign in 1`] = ` +
+ +
+`; diff --git a/src/core/client/auth/containers/AutoHeightContainer.tsx b/src/core/client/auth/containers/AutoHeightContainer.tsx new file mode 100644 index 000000000..309c89901 --- /dev/null +++ b/src/core/client/auth/containers/AutoHeightContainer.tsx @@ -0,0 +1,25 @@ +import { Component } from "react"; + +import resizePopup from "../dom/resizePopup"; + +/** + * A container that adapts the window height to the current body size + * when this is mounted or updated. + */ +export default class AutoHeightContainer extends Component { + private updateWindowSize() { + window.requestAnimationFrame(() => setTimeout(resizePopup, 0)); + } + + public componentDidMount() { + this.updateWindowSize(); + } + + public componentDidUpdate() { + this.updateWindowSize(); + } + + public render() { + return null; + } +} diff --git a/src/core/client/auth/containers/ForgotPasswordContainer.tsx b/src/core/client/auth/containers/ForgotPasswordContainer.tsx new file mode 100644 index 000000000..37b842160 --- /dev/null +++ b/src/core/client/auth/containers/ForgotPasswordContainer.tsx @@ -0,0 +1,11 @@ +import React, { Component } from "react"; +import ForgotPassword from "../components/ForgotPassword"; + +class ForgotPasswordContainer extends Component { + public render() { + // tslint:disable-next-line:no-empty + return {}} />; + } +} + +export default ForgotPasswordContainer; diff --git a/src/core/client/auth/containers/ResetPasswordContainer.tsx b/src/core/client/auth/containers/ResetPasswordContainer.tsx new file mode 100644 index 000000000..cfca96d9d --- /dev/null +++ b/src/core/client/auth/containers/ResetPasswordContainer.tsx @@ -0,0 +1,11 @@ +import React, { Component } from "react"; +import ResetPassword from "../components/ResetPassword"; + +class ResetPasswordContainer extends Component { + public render() { + // tslint:disable-next-line:no-empty + return {}} />; + } +} + +export default ResetPasswordContainer; diff --git a/src/core/client/auth/containers/SignInContainer.tsx b/src/core/client/auth/containers/SignInContainer.tsx new file mode 100644 index 000000000..fe2b87949 --- /dev/null +++ b/src/core/client/auth/containers/SignInContainer.tsx @@ -0,0 +1,40 @@ +import { FORM_ERROR } from "final-form"; +import React, { Component } from "react"; +import SignIn, { SignInForm } from "../components/SignIn"; +import { + SetViewMutation, + SignInMutation, + withSetViewMutation, + withSignInMutation, +} from "../mutations"; + +interface SignInContainerProps { + signIn: SignInMutation; + setView: SetViewMutation; +} + +class SignInContainer extends Component { + private onSubmit: SignInForm["onSubmit"] = async (input, form) => { + try { + await this.props.signIn(input); + return form.reset(); + } catch (error) { + return { [FORM_ERROR]: error.message }; + } + }; + private goToForgotPassword = () => + this.props.setView({ view: "FORGOT_PASSWORD" }); + private goToSignUp = () => this.props.setView({ view: "SIGN_UP" }); + public render() { + return ( + + ); + } +} + +const enhanced = withSetViewMutation(withSignInMutation(SignInContainer)); +export default enhanced; diff --git a/src/core/client/auth/containers/SignUpContainer.tsx b/src/core/client/auth/containers/SignUpContainer.tsx new file mode 100644 index 000000000..8be2d84ed --- /dev/null +++ b/src/core/client/auth/containers/SignUpContainer.tsx @@ -0,0 +1,33 @@ +import { FORM_ERROR } from "final-form"; +import React, { Component } from "react"; +import SignUp, { SignUpForm } from "../components/SignUp"; + +import { + SetViewMutation, + SignUpMutation, + withSetViewMutation, + withSignUpMutation, +} from "../mutations"; + +interface SignUpContainerProps { + signUp: SignUpMutation; + setView: SetViewMutation; +} + +class SignUpContainer extends Component { + private onSubmit: SignUpForm["onSubmit"] = async (input, form) => { + try { + return await this.props.signUp(input); + form.reset(); + } catch (error) { + return { [FORM_ERROR]: error.message }; + } + }; + private goToSignIn = () => this.props.setView({ view: "SIGN_IN" }); + public render() { + return ; + } +} + +const enhanced = withSetViewMutation(withSignUpMutation(SignUpContainer)); +export default enhanced; diff --git a/src/core/client/auth/dom/resizePopup.ts b/src/core/client/auth/dom/resizePopup.ts new file mode 100644 index 000000000..d9fc1ce6d --- /dev/null +++ b/src/core/client/auth/dom/resizePopup.ts @@ -0,0 +1,17 @@ +function resizePopup() { + const innerHeight = window.document.body.offsetHeight; + window.resizeTo( + window.outerWidth, + innerHeight + window.outerHeight - window.innerHeight + ); +} + +let resizedAlready = false; +export default function resizeOncePerFrame() { + if (resizedAlready) { + return; + } + resizedAlready = true; + requestAnimationFrame(() => setTimeout(() => (resizedAlready = false), 0)); + resizePopup(); +} diff --git a/src/core/client/auth/index.tsx b/src/core/client/auth/index.tsx index 44970a3fa..b30487719 100644 --- a/src/core/client/auth/index.tsx +++ b/src/core/client/auth/index.tsx @@ -9,9 +9,29 @@ import { } from "talk-framework/lib/bootstrap"; import AppContainer from "./containers/AppContainer"; +import resizePopup from "./dom/resizePopup"; import { initLocalState } from "./local"; import localesData from "./locales"; +/** + * Adapt popup height to current content every 100ms. + * + * The goal is to smooth out height inconsistensies e.g. when fonts + * are switched out or other resources being loaded that React has no influence + * over. + * + * This works in addition to which + * adapt popup height when React does an update. + */ +function pollPopupHeight(interval: number = 100) { + setTimeout(() => { + window.requestAnimationFrame(() => { + resizePopup(); + pollPopupHeight(interval); + }); + }, interval); +} + // This is called when the context is first initialized. async function init({ relayEnvironment }: TalkContext) { await initLocalState(relayEnvironment); @@ -32,6 +52,7 @@ async function main() { ); ReactDOM.render(, document.getElementById("app")); + pollPopupHeight(); } main(); diff --git a/src/core/client/auth/local/local.graphql b/src/core/client/auth/local/local.graphql index c536037d3..54c52fb90 100644 --- a/src/core/client/auth/local/local.graphql +++ b/src/core/client/auth/local/local.graphql @@ -2,6 +2,7 @@ enum View { SIGN_UP SIGN_IN FORGOT_PASSWORD + RESET_PASSWORD } type Local { diff --git a/src/core/client/auth/mutations/SetViewMutation.ts b/src/core/client/auth/mutations/SetViewMutation.ts index c5bb851fd..df95f18e1 100644 --- a/src/core/client/auth/mutations/SetViewMutation.ts +++ b/src/core/client/auth/mutations/SetViewMutation.ts @@ -6,7 +6,7 @@ import { LOCAL_ID } from "talk-framework/lib/relay/withLocalStateContainer"; export interface SetViewInput { // TODO: replace with generated typescript types. - view: "SIGN_IN" | "SIGN_UP" | "FORGOT_PASSWORD"; + view: "SIGN_IN" | "SIGN_UP" | "FORGOT_PASSWORD" | "RESET_PASSWORD"; } export type SetViewMutation = (input: SetViewInput) => Promise; diff --git a/src/core/client/auth/mutations/SignInMutation.ts b/src/core/client/auth/mutations/SignInMutation.ts index 6a88b9313..de02a33f5 100644 --- a/src/core/client/auth/mutations/SignInMutation.ts +++ b/src/core/client/auth/mutations/SignInMutation.ts @@ -17,6 +17,7 @@ export async function commit( window.close(); } catch (err) { postMessage.send("authError", err.toString(), window.opener); + throw err; } } diff --git a/src/core/client/auth/mutations/SignUpMutation.ts b/src/core/client/auth/mutations/SignUpMutation.ts index 68beaeae2..dc66aff31 100644 --- a/src/core/client/auth/mutations/SignUpMutation.ts +++ b/src/core/client/auth/mutations/SignUpMutation.ts @@ -1,3 +1,4 @@ +import { pick } from "lodash"; import { Environment } from "relay-runtime"; import { TalkContext } from "talk-framework/lib/bootstrap"; @@ -12,11 +13,15 @@ export async function commit( { rest, postMessage }: TalkContext ) { try { - const result = await signUp(rest, input); + const result = await signUp( + rest, + pick(input, "email", "password", "username") + ); postMessage.send("setAuthToken", result.token, window.opener); window.close(); } catch (err) { postMessage.send("authError", err.toString(), window.opener); + throw err; } } diff --git a/src/core/client/auth/test/__snapshots__/signIn.spec.tsx.snap b/src/core/client/auth/test/__snapshots__/signIn.spec.tsx.snap new file mode 100644 index 000000000..33a6f001a --- /dev/null +++ b/src/core/client/auth/test/__snapshots__/signIn.spec.tsx.snap @@ -0,0 +1,971 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`accepts correct password 1`] = ` +
+
+
+

+ Sign in to join the conversation +

+
+ + +
+
+ + +
+ +
+
+ +
+
+ + Don't have an account? <button>Sign Up</button> + +
+
+
+
+
+`; + +exports[`accepts valid email 1`] = ` +
+
+
+

+ Sign in to join the conversation +

+
+ + +
+
+ + +
+ + warning + + + This field is required. + +
+
+ +
+
+ +
+
+ + Don't have an account? <button>Sign Up</button> + +
+
+
+
+
+`; + +exports[`checks for invalid email 1`] = ` +
+
+
+

+ Sign in to join the conversation +

+
+ + +
+ + warning + + + Please enter a valid email address. + +
+
+
+ + +
+ + warning + + + This field is required. + +
+
+ +
+
+ +
+
+ + Don't have an account? <button>Sign Up</button> + +
+
+
+
+
+`; + +exports[`renders sign in form 1`] = ` +
+
+
+

+ Sign in to join the conversation +

+
+ + +
+
+ + +
+ +
+
+ +
+
+ + Don't have an account? <button>Sign Up</button> + +
+
+
+
+
+`; + +exports[`shows error when submitting empty form 1`] = ` +
+
+
+

+ Sign in to join the conversation +

+
+ + +
+ + warning + + + This field is required. + +
+
+
+ + +
+ + warning + + + This field is required. + +
+
+ +
+
+ +
+
+ + Don't have an account? <button>Sign Up</button> + +
+
+
+
+
+`; + +exports[`shows server error 1`] = ` +
+
+
+

+ Sign in to join the conversation +

+
+ + +
+
+ + +
+ +
+
+ +
+
+ + Don't have an account? <button>Sign Up</button> + +
+
+
+
+
+`; + +exports[`shows server error 2`] = ` +
+
+
+

+ Sign in to join the conversation +

+
+ Server Error +
+
+ + +
+
+ + +
+ +
+
+ +
+
+ + Don't have an account? <button>Sign Up</button> + +
+
+
+
+
+`; + +exports[`submits form successfully 1`] = ` +
+
+
+

+ Sign in to join the conversation +

+
+ Server Error +
+
+ + +
+
+ + +
+ +
+
+ +
+
+ + Don't have an account? <button>Sign Up</button> + +
+
+
+
+
+`; + +exports[`submits form successfully 2`] = ` +
+
+
+

+ Sign in to join the conversation +

+
+ + +
+
+ + +
+ +
+
+ +
+
+ + Don't have an account? <button>Sign Up</button> + +
+
+
+
+
+`; diff --git a/src/core/client/auth/test/__snapshots__/signUp.spec.tsx.snap b/src/core/client/auth/test/__snapshots__/signUp.spec.tsx.snap new file mode 100644 index 000000000..0d13ca1bc --- /dev/null +++ b/src/core/client/auth/test/__snapshots__/signUp.spec.tsx.snap @@ -0,0 +1,2355 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`accepts correct password 1`] = ` +
+
+
+

+ Sign up to join the conversation +

+
+ + +
+
+ +

+ A unique identifier displayed on your comments. You may use “_” and “.” +

+ +
+
+ +

+ Must be at least {$minLength} characters +

+ +
+
+ + +
+ + warning + + + This field is required. + +
+
+ +
+
+ + Already have an account? <button>Sign In </button> + +
+
+
+
+
+`; + +exports[`accepts correct password confirmation 1`] = ` +
+
+
+

+ Sign up to join the conversation +

+
+ + +
+
+ +

+ A unique identifier displayed on your comments. You may use “_” and “.” +

+ +
+
+ +

+ Must be at least {$minLength} characters +

+ +
+
+ + +
+ +
+
+ + Already have an account? <button>Sign In </button> + +
+
+
+
+
+`; + +exports[`accepts valid email 1`] = ` +
+
+
+

+ Sign up to join the conversation +

+
+ + +
+
+ +

+ A unique identifier displayed on your comments. You may use “_” and “.” +

+ +
+ + warning + + + This field is required. + +
+
+
+ +

+ Must be at least {$minLength} characters +

+ +
+ + warning + + + This field is required. + +
+
+
+ + +
+ + warning + + + This field is required. + +
+
+ +
+
+ + Already have an account? <button>Sign In </button> + +
+
+
+
+
+`; + +exports[`accepts valid username 1`] = ` +
+
+
+

+ Sign up to join the conversation +

+
+ + +
+
+ +

+ A unique identifier displayed on your comments. You may use “_” and “.” +

+ +
+
+ +

+ Must be at least {$minLength} characters +

+ +
+ + warning + + + This field is required. + +
+
+
+ + +
+ + warning + + + This field is required. + +
+
+ +
+
+ + Already have an account? <button>Sign In </button> + +
+
+
+
+
+`; + +exports[`checks for invalid characters in username 1`] = ` +
+
+
+

+ Sign up to join the conversation +

+
+ + +
+
+ +

+ A unique identifier displayed on your comments. You may use “_” and “.” +

+ +
+ + warning + + + Invalid characters. Try again. + +
+
+
+ +

+ Must be at least {$minLength} characters +

+ +
+ + warning + + + This field is required. + +
+
+
+ + +
+ + warning + + + This field is required. + +
+
+ +
+
+ + Already have an account? <button>Sign In </button> + +
+
+
+
+
+`; + +exports[`checks for invalid email 1`] = ` +
+
+
+

+ Sign up to join the conversation +

+
+ + +
+ + warning + + + Please enter a valid email address. + +
+
+
+ +

+ A unique identifier displayed on your comments. You may use “_” and “.” +

+ +
+ + warning + + + This field is required. + +
+
+
+ +

+ Must be at least {$minLength} characters +

+ +
+ + warning + + + This field is required. + +
+
+
+ + +
+ + warning + + + This field is required. + +
+
+ +
+
+ + Already have an account? <button>Sign In </button> + +
+
+
+
+
+`; + +exports[`checks for too long username 1`] = ` +
+
+
+

+ Sign up to join the conversation +

+
+ + +
+
+ +

+ A unique identifier displayed on your comments. You may use “_” and “.” +

+ +
+ + warning + + + Usernames cannot be longer than {$maxLength} characters. + +
+
+
+ +

+ Must be at least {$minLength} characters +

+ +
+ + warning + + + This field is required. + +
+
+
+ + +
+ + warning + + + This field is required. + +
+
+ +
+
+ + Already have an account? <button>Sign In </button> + +
+
+
+
+
+`; + +exports[`checks for too short password 1`] = ` +
+
+
+

+ Sign up to join the conversation +

+
+ + +
+
+ +

+ A unique identifier displayed on your comments. You may use “_” and “.” +

+ +
+
+ +

+ Must be at least {$minLength} characters +

+ +
+ + warning + + + Password must contain at least {$minLength} characters. + +
+
+
+ + +
+ + warning + + + This field is required. + +
+
+ +
+
+ + Already have an account? <button>Sign In </button> + +
+
+
+
+
+`; + +exports[`checks for too short username 1`] = ` +
+
+
+

+ Sign up to join the conversation +

+
+ + +
+
+ +

+ A unique identifier displayed on your comments. You may use “_” and “.” +

+ +
+ + warning + + + Usernames must contain at least {$minLength} characters. + +
+
+
+ +

+ Must be at least {$minLength} characters +

+ +
+ + warning + + + This field is required. + +
+
+
+ + +
+ + warning + + + This field is required. + +
+
+ +
+
+ + Already have an account? <button>Sign In </button> + +
+
+
+
+
+`; + +exports[`checks for wrong password confirmation 1`] = ` +
+
+
+

+ Sign up to join the conversation +

+
+ + +
+
+ +

+ A unique identifier displayed on your comments. You may use “_” and “.” +

+ +
+
+ +

+ Must be at least {$minLength} characters +

+ +
+
+ + +
+ + warning + + + Passwords do not match. Try again. + +
+
+ +
+
+ + Already have an account? <button>Sign In </button> + +
+
+
+
+
+`; + +exports[`renders sign up form 1`] = ` +
+
+
+

+ Sign up to join the conversation +

+
+ + +
+
+ +

+ A unique identifier displayed on your comments. You may use “_” and “.” +

+ +
+
+ +

+ Must be at least {$minLength} characters +

+ +
+
+ + +
+ +
+
+ + Already have an account? <button>Sign In </button> + +
+
+
+
+
+`; + +exports[`shows error when submitting empty form 1`] = ` +
+
+
+

+ Sign up to join the conversation +

+
+ + +
+ + warning + + + This field is required. + +
+
+
+ +

+ A unique identifier displayed on your comments. You may use “_” and “.” +

+ +
+ + warning + + + This field is required. + +
+
+
+ +

+ Must be at least {$minLength} characters +

+ +
+ + warning + + + This field is required. + +
+
+
+ + +
+ + warning + + + This field is required. + +
+
+ +
+
+ + Already have an account? <button>Sign In </button> + +
+
+
+
+
+`; + +exports[`shows server error 1`] = ` +
+
+
+

+ Sign up to join the conversation +

+
+ + +
+
+ +

+ A unique identifier displayed on your comments. You may use “_” and “.” +

+ +
+
+ +

+ Must be at least {$minLength} characters +

+ +
+
+ + +
+ +
+
+ + Already have an account? <button>Sign In </button> + +
+
+
+
+
+`; + +exports[`shows server error 2`] = ` +
+
+
+

+ Sign up to join the conversation +

+
+ Server Error +
+
+ + +
+
+ +

+ A unique identifier displayed on your comments. You may use “_” and “.” +

+ +
+
+ +

+ Must be at least {$minLength} characters +

+ +
+
+ + +
+ +
+
+ + Already have an account? <button>Sign In </button> + +
+
+
+
+
+`; + +exports[`submits form successfully 1`] = ` +
+
+
+

+ Sign up to join the conversation +

+
+ Server Error +
+
+ + +
+
+ +

+ A unique identifier displayed on your comments. You may use “_” and “.” +

+ +
+
+ +

+ Must be at least {$minLength} characters +

+ +
+
+ + +
+ +
+
+ + Already have an account? <button>Sign In </button> + +
+
+
+
+
+`; + +exports[`submits form successfully 2`] = ` +
+
+
+

+ Sign up to join the conversation +

+
+ + +
+
+ +

+ A unique identifier displayed on your comments. You may use “_” and “.” +

+ +
+
+ +

+ Must be at least {$minLength} characters +

+ +
+
+ + +
+ +
+
+ + Already have an account? <button>Sign In </button> + +
+
+
+
+
+`; diff --git a/src/core/client/auth/test/createEnvironment.ts b/src/core/client/auth/test/createEnvironment.ts new file mode 100644 index 000000000..700b354c9 --- /dev/null +++ b/src/core/client/auth/test/createEnvironment.ts @@ -0,0 +1,28 @@ +import { IResolvers } from "graphql-tools"; +import { Environment, RecordProxy, RecordSourceProxy } from "relay-runtime"; +import { createRelayEnvironment } from "talk-framework/testHelpers"; + +interface CreateEnvironmentParams { + logNetwork?: boolean; + resolvers?: IResolvers; + initLocalState?: ( + local: RecordProxy, + source: RecordSourceProxy, + environment: Environment + ) => void; +} + +export default function createEnvironment(params: CreateEnvironmentParams) { + return createRelayEnvironment({ + network: { + logNetwork: params.logNetwork, + resolvers: params.resolvers || {}, + projectName: "tenant", + }, + initLocalState: (localRecord, source, environment) => { + if (params.initLocalState) { + params.initLocalState(localRecord, source, environment); + } + }, + }); +} diff --git a/src/core/client/auth/test/navigation.disabledspec.tsx b/src/core/client/auth/test/navigation.disabledspec.tsx new file mode 100644 index 000000000..0abf24548 --- /dev/null +++ b/src/core/client/auth/test/navigation.disabledspec.tsx @@ -0,0 +1,59 @@ +// Enable after this is solved: https://github.com/projectfluent/fluent.js/issues/280 + +import React from "react"; +import TestRenderer from "react-test-renderer"; +import { RecordProxy } from "relay-runtime"; + +import AppContainer from "talk-auth/containers/AppContainer"; +import { TalkContext, TalkContextProvider } from "talk-framework/lib/bootstrap"; +import { PostMessageService } from "talk-framework/lib/postMessage"; +import { RestClient } from "talk-framework/lib/rest"; +import { createInMemoryStorage } from "talk-framework/lib/storage"; + +import createEnvironment from "./createEnvironment"; + +const environment = createEnvironment({ + initLocalState: (localRecord: RecordProxy) => { + localRecord.setValue("SIGN_IN", "view"); + }, +}); + +const context: TalkContext = { + relayEnvironment: environment, + localeBundles: [], + localStorage: createInMemoryStorage(), + sessionStorage: createInMemoryStorage(), + rest: new RestClient("http://localhost/api"), + postMessage: new PostMessageService(), +}; + +const testRenderer = TestRenderer.create( + + + +); + +it("renders sign in form", async () => { + expect(testRenderer.toJSON()).toMatchSnapshot(); +}); + +it("navigates to sign up form", async () => { + testRenderer.root + .findByProps({ id: "signIn-gotoSignUpButton" }) + .props.onClick(); + expect(testRenderer.toJSON()).toMatchSnapshot(); +}); + +it("navigates to sign in form", async () => { + testRenderer.root + .findByProps({ id: "signUp-gotoSignInButton" }) + .props.onClick(); + expect(testRenderer.toJSON()).toMatchSnapshot(); +}); + +it("navigates to forgot password form", async () => { + testRenderer.root + .findByProps({ id: "signIn-gotoForgotPasswordButton" }) + .props.onClick(); + expect(testRenderer.toJSON()).toMatchSnapshot(); +}); diff --git a/src/core/client/auth/test/signIn.spec.tsx b/src/core/client/auth/test/signIn.spec.tsx new file mode 100644 index 000000000..90447438a --- /dev/null +++ b/src/core/client/auth/test/signIn.spec.tsx @@ -0,0 +1,140 @@ +import React from "react"; +import TestRenderer, { ReactTestInstance } from "react-test-renderer"; +import { RecordProxy } from "relay-runtime"; +import sinon from "sinon"; + +import AppContainer from "talk-auth/containers/AppContainer"; +import { animationFrame, timeout } from "talk-common/utils"; +import { TalkContext, TalkContextProvider } from "talk-framework/lib/bootstrap"; +import { PostMessageService } from "talk-framework/lib/postMessage"; +import { RestClient } from "talk-framework/lib/rest"; +import { createInMemoryStorage } from "talk-framework/lib/storage"; + +import createEnvironment from "./createEnvironment"; + +const environment = createEnvironment({ + initLocalState: (localRecord: RecordProxy) => { + localRecord.setValue("SIGN_IN", "view"); + }, +}); + +const context: TalkContext = { + relayEnvironment: environment, + localeBundles: [], + localStorage: createInMemoryStorage(), + sessionStorage: createInMemoryStorage(), + rest: new RestClient("http://localhost/api"), + postMessage: new PostMessageService(), +}; + +const testRenderer = TestRenderer.create( + + + +); + +const inputPredicate = (name: string) => (n: ReactTestInstance) => { + return n.props.name === name && n.props.onChange; +}; + +const form = testRenderer.root.findByType("form"); + +it("renders sign in form", async () => { + expect(testRenderer.toJSON()).toMatchSnapshot(); +}); + +it("shows error when submitting empty form", async () => { + form.props.onSubmit(); + expect(testRenderer.toJSON()).toMatchSnapshot(); +}); + +it("checks for invalid email", async () => { + form + .find(inputPredicate("email")) + .props.onChange({ target: { value: "invalid-email" } }); + expect(testRenderer.toJSON()).toMatchSnapshot(); +}); + +it("accepts valid email", async () => { + form + .find(inputPredicate("email")) + .props.onChange({ target: { value: "hans@test.com" } }); + expect(testRenderer.toJSON()).toMatchSnapshot(); +}); + +it("accepts correct password", async () => { + form + .find(inputPredicate("password")) + .props.onChange({ target: { value: "testtest" } }); + expect(testRenderer.toJSON()).toMatchSnapshot(); +}); + +it("shows server error", async () => { + const windowMock = sinon.mock(window); + windowMock.expects("resizeTo"); + + const error = new Error("Server Error"); + const restMock = sinon.mock(context.rest); + restMock + .expects("fetch") + .withArgs("/tenant/auth/local", { + method: "POST", + body: { + email: "hans@test.com", + password: "testtest", + }, + }) + .once() + .throws(error); + + const postMessageMock = sinon.mock(context.postMessage); + postMessageMock + .expects("send") + .withArgs("authError", error.toString(), window.opener) + .once(); + + form.props.onSubmit(); + expect(testRenderer.toJSON()).toMatchSnapshot(); + // popup resize will be triggered if we wait for the animation frame first. + await animationFrame(); + await timeout(); + expect(testRenderer.toJSON()).toMatchSnapshot(); + restMock.verify(); + postMessageMock.verify(); + windowMock.verify(); +}); + +it("submits form successfully", async () => { + const windowMock = sinon.mock(window); + windowMock.expects("close").once(); + windowMock.expects("resizeTo"); + + const restMock = sinon.mock(context.rest); + restMock + .expects("fetch") + .withArgs("/tenant/auth/local", { + method: "POST", + body: { + email: "hans@test.com", + password: "testtest", + }, + }) + .once() + .returns({ token: "auth-token" }); + + const postMessageMock = sinon.mock(context.postMessage); + postMessageMock + .expects("send") + .withArgs("setAuthToken", "auth-token", window.opener) + .once(); + + form.props.onSubmit(); + expect(testRenderer.toJSON()).toMatchSnapshot(); + // popup resize will be triggered if we wait for the animation frame first. + await animationFrame(); + await timeout(); + expect(testRenderer.toJSON()).toMatchSnapshot(); + restMock.verify(); + postMessageMock.verify(); + windowMock.verify(); +}); diff --git a/src/core/client/auth/test/signUp.spec.tsx b/src/core/client/auth/test/signUp.spec.tsx new file mode 100644 index 000000000..307f2c4b2 --- /dev/null +++ b/src/core/client/auth/test/signUp.spec.tsx @@ -0,0 +1,191 @@ +import React from "react"; +import TestRenderer, { ReactTestInstance } from "react-test-renderer"; +import { RecordProxy } from "relay-runtime"; +import sinon from "sinon"; + +import AppContainer from "talk-auth/containers/AppContainer"; +import { animationFrame, timeout } from "talk-common/utils"; +import { TalkContext, TalkContextProvider } from "talk-framework/lib/bootstrap"; +import { PostMessageService } from "talk-framework/lib/postMessage"; +import { RestClient } from "talk-framework/lib/rest"; +import { createInMemoryStorage } from "talk-framework/lib/storage"; + +import createEnvironment from "./createEnvironment"; + +const environment = createEnvironment({ + initLocalState: (localRecord: RecordProxy) => { + localRecord.setValue("SIGN_UP", "view"); + }, +}); + +const context: TalkContext = { + relayEnvironment: environment, + localeBundles: [], + localStorage: createInMemoryStorage(), + sessionStorage: createInMemoryStorage(), + rest: new RestClient("http://localhost/api"), + postMessage: new PostMessageService(), +}; + +const testRenderer = TestRenderer.create( + + + +); + +const inputPredicate = (name: string) => (n: ReactTestInstance) => { + return n.props.name === name && n.props.onChange; +}; + +const form = testRenderer.root.findByType("form"); + +it("renders sign up form", async () => { + expect(testRenderer.toJSON()).toMatchSnapshot(); +}); + +it("shows error when submitting empty form", async () => { + form.props.onSubmit(); + expect(testRenderer.toJSON()).toMatchSnapshot(); +}); + +it("checks for invalid email", async () => { + form + .find(inputPredicate("email")) + .props.onChange({ target: { value: "invalid-email" } }); + expect(testRenderer.toJSON()).toMatchSnapshot(); +}); + +it("accepts valid email", async () => { + form + .find(inputPredicate("email")) + .props.onChange({ target: { value: "hans@test.com" } }); + expect(testRenderer.toJSON()).toMatchSnapshot(); +}); + +it("checks for too short username", async () => { + form + .find(inputPredicate("username")) + .props.onChange({ target: { value: "u" } }); + expect(testRenderer.toJSON()).toMatchSnapshot(); +}); + +it("checks for too long username", async () => { + form + .find(inputPredicate("username")) + .props.onChange({ target: { value: "a".repeat(100) } }); + expect(testRenderer.toJSON()).toMatchSnapshot(); +}); + +it("checks for invalid characters in username", async () => { + form + .find(inputPredicate("username")) + .props.onChange({ target: { value: "$%$§$%$§%" } }); + expect(testRenderer.toJSON()).toMatchSnapshot(); +}); + +it("accepts valid username", async () => { + form + .find(inputPredicate("username")) + .props.onChange({ target: { value: "hans" } }); + expect(testRenderer.toJSON()).toMatchSnapshot(); +}); + +it("checks for too short password", async () => { + form + .find(inputPredicate("password")) + .props.onChange({ target: { value: "pass" } }); + expect(testRenderer.toJSON()).toMatchSnapshot(); +}); + +it("accepts correct password", async () => { + form + .find(inputPredicate("password")) + .props.onChange({ target: { value: "testtest" } }); + expect(testRenderer.toJSON()).toMatchSnapshot(); +}); + +it("checks for wrong password confirmation", async () => { + form + .find(inputPredicate("confirmPassword")) + .props.onChange({ target: { value: "not-matching" } }); + expect(testRenderer.toJSON()).toMatchSnapshot(); +}); + +it("accepts correct password confirmation", async () => { + form + .find(inputPredicate("confirmPassword")) + .props.onChange({ target: { value: "testtest" } }); + expect(testRenderer.toJSON()).toMatchSnapshot(); +}); + +it("shows server error", async () => { + const windowMock = sinon.mock(window); + windowMock.expects("resizeTo"); + + const error = new Error("Server Error"); + const restMock = sinon.mock(context.rest); + restMock + .expects("fetch") + .withArgs("/tenant/auth/local/signup", { + method: "POST", + body: { + username: "hans", + email: "hans@test.com", + password: "testtest", + }, + }) + .once() + .throws(error); + + const postMessageMock = sinon.mock(context.postMessage); + postMessageMock + .expects("send") + .withArgs("authError", error.toString(), window.opener) + .once(); + + form.props.onSubmit(); + expect(testRenderer.toJSON()).toMatchSnapshot(); + // popup resize will be triggered if we wait for the animation frame first. + await animationFrame(); + await timeout(); + expect(testRenderer.toJSON()).toMatchSnapshot(); + restMock.verify(); + postMessageMock.verify(); + windowMock.verify(); +}); + +it("submits form successfully", async () => { + const windowMock = sinon.mock(window); + windowMock.expects("close").once(); + windowMock.expects("resizeTo"); + + const restMock = sinon.mock(context.rest); + restMock + .expects("fetch") + .withArgs("/tenant/auth/local/signup", { + method: "POST", + body: { + username: "hans", + email: "hans@test.com", + password: "testtest", + }, + }) + .once() + .returns({ token: "auth-token" }); + + const postMessageMock = sinon.mock(context.postMessage); + postMessageMock + .expects("send") + .withArgs("setAuthToken", "auth-token", window.opener) + .once(); + + form.props.onSubmit(); + expect(testRenderer.toJSON()).toMatchSnapshot(); + // popup resize will be triggered if we wait for the animation frame first. + await animationFrame(); + await timeout(); + expect(testRenderer.toJSON()).toMatchSnapshot(); + restMock.verify(); + postMessageMock.verify(); + windowMock.verify(); +}); diff --git a/src/core/client/framework/lib/bootstrap/TalkContext.tsx b/src/core/client/framework/lib/bootstrap/TalkContext.tsx index 6c0d8f41d..a77a14576 100644 --- a/src/core/client/framework/lib/bootstrap/TalkContext.tsx +++ b/src/core/client/framework/lib/bootstrap/TalkContext.tsx @@ -1,5 +1,5 @@ import { LocalizationProvider } from "fluent-react/compat"; -import { MessageContext } from "fluent/compat"; +import { FluentBundle } from "fluent/compat"; import { Child as PymChild } from "pym.js"; import React, { StatelessComponent } from "react"; import { MediaQueryMatchers } from "react-responsive"; @@ -15,8 +15,8 @@ export interface TalkContext { /** relayEnvironment for our relay framework. */ relayEnvironment: Environment; - /** localMessages for our i18n framework. */ - localeMessages: MessageContext[]; + /** localeBundles for our i18n framework. */ + localeBundles: FluentBundle[]; /** formatter for timeago. */ timeagoFormatter?: Formatter; @@ -61,7 +61,7 @@ export const TalkContextProvider: StatelessComponent<{ value: TalkContext; }> = ({ value, children }) => ( - + cx; +let decorateWarnMissing = (bundle: FluentBundle) => bundle; // Warn about missing locales if we are not in production. if (process.env.NODE_ENV !== "production") { decorateWarnMissing = (() => { const warnings: string[] = []; - return (cx: MessageContext) => { - const original = cx.hasMessage; - cx.hasMessage = (id: string) => { - const result = original.apply(cx, [id]); + return (bundle: FluentBundle) => { + const original = bundle.hasMessage; + bundle.hasMessage = (id: string) => { + const result = original.apply(bundle, [id]); if (!result) { - const warn = `${cx.locales} translation for key "${id}" not found`; + const warn = `${ + bundle.locales + } translation for key "${id}" not found`; if (!warnings.includes(warn)) { // tslint:disable:next-line: no-console console.warn(warn); @@ -67,7 +69,7 @@ if (process.env.NODE_ENV !== "production") { } return result; }; - return cx; + return bundle; }; })(); } @@ -79,21 +81,21 @@ if (process.env.NODE_ENV !== "production") { * * Use it in conjunction with `negotiateLanguages`. */ -export async function generateMessages( +export async function generateBundles( locales: ReadonlyArray, data: LocalesData -): Promise { +): Promise { const promises = []; for (const locale of locales) { - const cx = new MessageContext(locale); + const bundle = new FluentBundle(locale); if (locale in data.bundled) { - cx.addMessages(data.bundled[locale]); - promises.push(decorateWarnMissing(cx)); + bundle.addMessages(data.bundled[locale]); + promises.push(decorateWarnMissing(bundle)); } else if (locale in data.loadables) { const content = await data.loadables[locale](); - cx.addMessages(content); - promises.push(decorateWarnMissing(cx)); + bundle.addMessages(content); + promises.push(decorateWarnMissing(bundle)); } else { throw Error(`Locale ${locale} not available`); } diff --git a/src/core/client/framework/lib/messages.tsx b/src/core/client/framework/lib/messages.tsx index 316cd6e8a..28beed2f4 100644 --- a/src/core/client/framework/lib/messages.tsx +++ b/src/core/client/framework/lib/messages.tsx @@ -12,8 +12,44 @@ export const VALIDATION_REQUIRED = () => ( ); -export const VALIDATION_TOO_SHORT = () => ( +export const VALIDATION_TOO_SHORT = (minLength: number) => ( - This field is too short. + {"This field must contain at least {$minLength} characters."} + +); + +export const INVALID_EMAIL = () => ( + + Please enter a valid email address. + +); + +export const INVALID_CHARACTERS = () => ( + + Invalid characters. Try again. + +); + +export const USERNAME_TOO_SHORT = (minLength: number) => ( + + {"Usernames must contain at least {$minLength} characters."} + +); + +export const USERNAME_TOO_LONG = (maxLength: number) => ( + + {"Usernames cannot be longer than {$maxLength} characters."} + +); + +export const PASSWORD_TOO_SHORT = (minLength: number) => ( + + {"Password must contain at least {$minLength} characters."} + +); + +export const PASSWORDS_DO_NOT_MATCH = () => ( + + Passwords do not match. Try again. ); diff --git a/src/core/client/framework/lib/rest.ts b/src/core/client/framework/lib/rest.ts index 6ba52b958..c08e3c995 100644 --- a/src/core/client/framework/lib/rest.ts +++ b/src/core/client/framework/lib/rest.ts @@ -17,22 +17,27 @@ const buildOptions = (inputOptions: RequestInit = {}) => { return options; }; -const handleResp = (res: Response) => { - if (res.status > 399) { - return res.json().then((err: any) => { - // TODO: sync error handling with server. - const message = err.message || err.error || res.status; - const error = new Error(message); - throw error; - }); - } else if (res.status === 204) { - return res.text(); - } else { - return res.json(); +// TODO (bc): Wrap response errors into error objects once server errors have been defined + +const handleResp = async (res: Response) => { + if (res.status === 404) { + const response = await res.text(); + throw new Error(response); } + + if (!res.ok) { + const response = await res.json(); + throw new Error(response.error); + } + + if (res.status === 204) { + return res.text(); + } + + return res.json(); }; -type PartialRequestInit = Overwrite, { body: any }>; +type PartialRequestInit = Overwrite, { body?: any }>; export class RestClient { public readonly uri: string; @@ -43,7 +48,10 @@ export class RestClient { this.tokenGetter = tokenGetter; } - public fetch(path: string, options: PartialRequestInit): Promise { + public async fetch( + path: string, + options: PartialRequestInit + ): Promise { let opts = options; if (this.tokenGetter) { opts = merge({}, options, { @@ -52,6 +60,7 @@ export class RestClient { }, }); } - return fetch(`${this.uri}${path}`, buildOptions(opts)).then(handleResp); + const response = await fetch(`${this.uri}${path}`, buildOptions(opts)); + return handleResp(response); } } diff --git a/src/core/client/framework/lib/validation.tsx b/src/core/client/framework/lib/validation.tsx index 54f75a01c..6634e9fde 100644 --- a/src/core/client/framework/lib/validation.tsx +++ b/src/core/client/framework/lib/validation.tsx @@ -1,5 +1,13 @@ import { ReactNode } from "react"; -import { VALIDATION_REQUIRED } from "./messages"; +import { + INVALID_CHARACTERS, + INVALID_EMAIL, + PASSWORD_TOO_SHORT, + PASSWORDS_DO_NOT_MATCH, + USERNAME_TOO_LONG, + USERNAME_TOO_SHORT, + VALIDATION_REQUIRED, +} from "./messages"; type Validator = (v: T, values: V) => ReactNode; @@ -31,3 +39,60 @@ export function composeValidators( * required is a Validator that checks that the value is truthy. */ export const required = createValidator(v => !!v, VALIDATION_REQUIRED()); + +/** + * validateEmail is a Validator that checks that the value is an email. + */ +export const validateEmail = createValidator( + v => /^.+@.+\..+$/.test(v), + INVALID_EMAIL() +); + +/** + * validateUsernameCharacters is a Validator that checks that the username only contains valid characters. + */ +export const validateUsernameCharacters = createValidator( + v => /^[a-zA-Z0-9_.]+$/.test(v), + INVALID_CHARACTERS() +); + +/** + * validateUsernameMinLength is a Validator that checks that the username has a min length of characters + */ +export const validateUsernameMinLength = createValidator( + v => v.length >= 3, + USERNAME_TOO_SHORT(3) +); + +/** + * validateUsernameMaxLength is a Validator that checks that the username has a max length of characters + */ +export const validateUsernameMaxLength = createValidator( + v => v.length <= 20, + USERNAME_TOO_LONG(20) +); + +/** + * validateUsername is a Validator that checks that the username is valid. + */ +export const validateUsername = composeValidators( + validateUsernameCharacters, + validateUsernameMinLength, + validateUsernameMaxLength +); + +/** + * validateUsername is a Validator that checks that the value is a valid username. + */ +export const validatePassword = createValidator( + v => v.length >= 8, + PASSWORD_TOO_SHORT(8) +); + +/**s + * validateUsername is a Validator that checks that the value is a valid username. + */ +export const validateEqualPasswords = createValidator( + (v, values) => v === values.password, + PASSWORDS_DO_NOT_MATCH() +); diff --git a/src/core/client/framework/mutations/SetAuthTokenMutation.ts b/src/core/client/framework/mutations/SetAuthTokenMutation.ts index 223bc4054..90e520155 100644 --- a/src/core/client/framework/mutations/SetAuthTokenMutation.ts +++ b/src/core/client/framework/mutations/SetAuthTokenMutation.ts @@ -23,6 +23,8 @@ export async function commit( } else { localStorage.removeItem("authToken"); } + // Increment auth revision to indicate a change in auth state. + record.setValue(record.getValue("authRevision") + 1, "authRevision"); // Force gc to trigger. environment diff --git a/src/core/client/framework/mutations/SignOutMutation.ts b/src/core/client/framework/mutations/SignOutMutation.ts new file mode 100644 index 000000000..2f170e32e --- /dev/null +++ b/src/core/client/framework/mutations/SignOutMutation.ts @@ -0,0 +1,18 @@ +import { Environment } from "relay-runtime"; +import { TalkContext } from "talk-framework/lib/bootstrap"; +import { createMutationContainer } from "talk-framework/lib/relay"; +import signOut from "../rest/signOut"; +import { commit as setAuthToken } from "./SetAuthTokenMutation"; + +export type SignOutMutation = () => Promise; + +export async function commit( + environment: Environment, + input: undefined, + ctx: TalkContext +) { + await signOut(ctx.rest); + await setAuthToken(environment, { authToken: "" }, ctx); +} + +export const withSignOutMutation = createMutationContainer("signOut", commit); diff --git a/src/core/client/framework/mutations/index.ts b/src/core/client/framework/mutations/index.ts index 21875e6d8..b128402e6 100644 --- a/src/core/client/framework/mutations/index.ts +++ b/src/core/client/framework/mutations/index.ts @@ -3,3 +3,4 @@ export { SetAuthTokenMutation, SetAuthTokenInput, } from "./SetAuthTokenMutation"; +export { withSignOutMutation, SignOutMutation } from "./SignOutMutation"; diff --git a/src/core/client/framework/rest/index.ts b/src/core/client/framework/rest/index.ts index 0b3032bb5..b50cbdbe9 100644 --- a/src/core/client/framework/rest/index.ts +++ b/src/core/client/framework/rest/index.ts @@ -1,2 +1,3 @@ export { default as signIn, SignInInput } from "./signIn"; export { default as signUp, SignUpInput } from "./signUp"; +export { default as signOut } from "./signOut"; diff --git a/src/core/client/framework/rest/signOut.ts b/src/core/client/framework/rest/signOut.ts new file mode 100644 index 000000000..0df1be920 --- /dev/null +++ b/src/core/client/framework/rest/signOut.ts @@ -0,0 +1,7 @@ +import { RestClient } from "../lib/rest"; + +export default function signOut(rest: RestClient) { + return rest.fetch("/tenant/auth", { + method: "DELETE", + }); +} diff --git a/src/core/client/stream/components/App.tsx b/src/core/client/stream/components/App.tsx index 39272cb17..a09c49149 100644 --- a/src/core/client/stream/components/App.tsx +++ b/src/core/client/stream/components/App.tsx @@ -2,7 +2,6 @@ import * as React from "react"; import { StatelessComponent } from "react"; import { Flex } from "talk-ui/components"; - import PermalinkViewQuery from "../queries/PermalinkViewQuery"; import StreamQuery from "../queries/StreamQuery"; diff --git a/src/core/client/stream/components/Comment/Comment.css b/src/core/client/stream/components/Comment/Comment.css index 5c0f67095..821be5613 100644 --- a/src/core/client/stream/components/Comment/Comment.css +++ b/src/core/client/stream/components/Comment/Comment.css @@ -1,5 +1,4 @@ .root { - width: 100%; } .footer { margin-top: var(--spacing-unit); diff --git a/src/core/client/stream/components/Comment/Username.tsx b/src/core/client/stream/components/Comment/Username.tsx index f19c23024..728944ea2 100644 --- a/src/core/client/stream/components/Comment/Username.tsx +++ b/src/core/client/stream/components/Comment/Username.tsx @@ -16,7 +16,7 @@ const Username: StatelessComponent = props => { {props.children} diff --git a/src/core/client/stream/components/Comment/__snapshots__/TopBar.spec.tsx.snap b/src/core/client/stream/components/Comment/__snapshots__/TopBar.spec.tsx.snap index 2e6694137..9ce1f79c5 100644 --- a/src/core/client/stream/components/Comment/__snapshots__/TopBar.spec.tsx.snap +++ b/src/core/client/stream/components/Comment/__snapshots__/TopBar.spec.tsx.snap @@ -2,28 +2,20 @@ exports[`renders correctly on big screens 1`] = `
-
-
- Hello World -
+
+ Hello World
`; exports[`renders correctly on small screens 1`] = `
-
-
- Hello World -
+
+ Hello World
`; diff --git a/src/core/client/stream/components/PermalinkButton/PermalinkPopover.css b/src/core/client/stream/components/PermalinkButton/PermalinkPopover.css index c2afa31b2..3fad6d3be 100644 --- a/src/core/client/stream/components/PermalinkButton/PermalinkPopover.css +++ b/src/core/client/stream/components/PermalinkButton/PermalinkPopover.css @@ -1,5 +1,4 @@ .root { - width: 100%; } .textField { diff --git a/src/core/client/stream/components/PermalinkView.tsx b/src/core/client/stream/components/PermalinkView.tsx index 4d3035b8d..362efa562 100644 --- a/src/core/client/stream/components/PermalinkView.tsx +++ b/src/core/client/stream/components/PermalinkView.tsx @@ -1,7 +1,7 @@ import { Localized } from "fluent-react/compat"; import React, { MouseEvent, StatelessComponent } from "react"; -import { Button, Flex, Typography } from "talk-ui/components"; +import { Button, Typography } from "talk-ui/components"; import CommentContainer from "../containers/CommentContainer"; import * as styles from "./PermalinkView.css"; @@ -41,11 +41,7 @@ const PermalinkView: StatelessComponent = ({ Comment not found )} - {comment && ( - - - - )} + {comment && }
); }; diff --git a/src/core/client/stream/components/PostCommentForm.css b/src/core/client/stream/components/PostCommentForm.css index e813f4ea7..8c35347ff 100644 --- a/src/core/client/stream/components/PostCommentForm.css +++ b/src/core/client/stream/components/PostCommentForm.css @@ -1,18 +1,17 @@ .root { - width: 100%; } .textarea { composes: bodyCopy from "talk-ui/shared/typography.css"; - display: block; - height: 100px; + height: 150px; width: 100%; box-sizing: border-box; - margin-bottom: var(--spacing-unit); + padding: var(--spacing-unit); + border-radius: var(--round-corners); + resize: vertical; } -.postButtonContainer { - display: flex; - justify-content: flex-end; +.poweredBy { + margin-top: calc(-0.5 * var(--spacing-unit)); } diff --git a/src/core/client/stream/components/PostCommentForm.tsx b/src/core/client/stream/components/PostCommentForm.tsx index 72bc0ad70..3eab2ddd8 100644 --- a/src/core/client/stream/components/PostCommentForm.tsx +++ b/src/core/client/stream/components/PostCommentForm.tsx @@ -1,13 +1,11 @@ import { Localized } from "fluent-react/compat"; -import * as React from "react"; -import { StatelessComponent } from "react"; +import React, { StatelessComponent } from "react"; import { Field, Form } from "react-final-form"; - import { OnSubmit } from "talk-framework/lib/form"; import { required } from "talk-framework/lib/validation"; -import { Button, Typography } from "talk-ui/components"; - +import { Button, Flex, HorizontalGutter, Typography } from "talk-ui/components"; import * as styles from "./PostCommentForm.css"; +import PoweredBy from "./PoweredBy"; interface FormProps { body: string; @@ -21,31 +19,44 @@ const PostCommentForm: StatelessComponent = props => (
{({ handleSubmit, submitting }) => ( - - {({ input, meta }) => ( -
-