diff --git a/packages/react-meteor-accounts/.gitignore b/packages/react-meteor-accounts/.gitignore new file mode 100644 index 00000000..3c3629e6 --- /dev/null +++ b/packages/react-meteor-accounts/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/packages/react-meteor-accounts/.meteorignore b/packages/react-meteor-accounts/.meteorignore new file mode 100644 index 00000000..f71737da --- /dev/null +++ b/packages/react-meteor-accounts/.meteorignore @@ -0,0 +1,3 @@ +node_modules +package.json +package-lock.json diff --git a/packages/react-meteor-accounts/.versions b/packages/react-meteor-accounts/.versions new file mode 100644 index 00000000..785711d1 --- /dev/null +++ b/packages/react-meteor-accounts/.versions @@ -0,0 +1,18 @@ +babel-compiler@7.7.0 +babel-runtime@1.5.0 +dynamic-import@0.7.2 +ecmascript@0.15.3 +ecmascript-runtime@0.8.0 +ecmascript-runtime-client@0.12.1 +ecmascript-runtime-server@0.11.0 +fetch@0.1.1 +inter-process-messaging@0.1.1 +meteor@1.10.0 +modern-browsers@0.1.7 +modules@0.17.0 +modules-runtime@0.12.0 +promise@0.12.0 +react-accounts@1.0.0-beta.1 +react-fast-refresh@0.1.1 +tracker@1.2.0 +typescript@4.3.5 diff --git a/packages/react-meteor-accounts/CHANGELOG.md b/packages/react-meteor-accounts/CHANGELOG.md new file mode 100644 index 00000000..6a16211b --- /dev/null +++ b/packages/react-meteor-accounts/CHANGELOG.md @@ -0,0 +1,22 @@ +# Changelog + +Release versions follow [Semantic Versioning 2.0.0 guidelines](https://semver.org/). + +## v1.0.0-rc.1 + +- `useLoggingIn`: Added implementation +- `useLoggingOut`: Added implementation +- `withLoggingIn`: Added implementation +- `withLoggingOut`: Added implementation +- improved tests and readme + +## v1.0.0-beta.1 + +2021-10-20 (date of last commit) + +### Features + +- `useUserId`: initial implementation. +- `useUser`: initial implementation. +- `withUserId`: initial implementation. +- `withUser`: initial implementation. diff --git a/packages/react-meteor-accounts/README.md b/packages/react-meteor-accounts/README.md new file mode 100644 index 00000000..f6102d42 --- /dev/null +++ b/packages/react-meteor-accounts/README.md @@ -0,0 +1,332 @@ +# react-meteor-accounts + +Simple hooks and higher-order components (HOCs) for getting reactive, stateful values of Meteor's Accounts data sources. + +## Table of Contents + +- [Installation](#installation) + - [Peer npm dependencies](#peer-npm-dependencies) + - [Changelog](#changelog) +- [Usage](#usage) + - [`useUser`](#useuser) + - [`useUserId`](#useuserid) + - [`useLoggingIn`](#useloggingin) + - [`useLoggingOut`](#useloggingout) + - [`withUser`](#withuser) + - [`withUserId`](#withuserid) + - [`withLoggingIn`](#withloggingin) + - [`withLoggingOut`](#withloggingout) + +## Installation + +Install the package from Atmosphere: + +```shell +meteor add mdg:react-meteor-accounts +``` + +### Peer npm dependencies + +Install React if you have not already: + +```shell +meteor npm install react +``` + +_Note:_ The minimum supported version of React is v16.8 ("the one with hooks"). + +### Changelog + +For recent changes, check the [changelog](./CHANGELOG.md). + +## Usage + +Utilities for each data source are available for the two ways of writing React components: hooks and higher-order components (HOCs). Hooks can only be used in functional components. HOCs can be used for both functional and class components, but are primarily for the latter. + +_Note:_ All HOCs forward refs. + +### useUser() + +Get a stateful value of the current user record. A hook. Uses [`Meteor.user`](https://docs.meteor.com/api/accounts.html#Meteor-user), a reactive data source. + +- Arguments: *none*. +- Returns: `object | null`. + +Example: + +```tsx +import React from 'react'; +import { useUser } from 'meteor/mdg:react-meteor-accounts'; + +function Foo() { + const user = useUser(); + + if (user === null) { + return

Log in

; + } + + return

Hello {user.username}

; +} +``` + +TypeScript signature: + +```ts +function useUser(): Meteor.User | null; +``` + +### useUserId() + +Get a stateful value of the current user id. A hook. Uses [`Meteor.userId`](https://docs.meteor.com/api/accounts.html#Meteor-userId), a reactive data source. + +- Arguments: *none*. +- Returns: `string | null`. + +Example: + +```tsx +import React from 'react'; +import { useUserId } from 'meteor/mdg:react-meteor-accounts'; + +function Foo() { + const userId = useUserId(); + + return ( +
+

Account Details

+ {userId ? ( +

Your unique account id is {userId}.

+ ) : ( +

Log-in to view your account details.

+ )} +
+ ); +} +``` + +TypeScript signature: + +```ts +function useUserId(): string | null; +``` + +### useLoggingIn() + +Get a stateful value of whether a login method (e.g. `loginWith`) is currently in progress. A hook. Uses [`Meteor.loggingIn`](https://docs.meteor.com/api/accounts.html#Meteor-loggingIn), a reactive data source. + +- Arguments: *none*. +- Returns: `boolean`. + +Example: + +```tsx +import React from 'react'; +import { useLoggingIn } from 'meteor/mdg:react-meteor-accounts'; + +function Foo() { + const loggingIn = useLoggingIn(); + + if (!loggingIn) { + return null; + } + + return ( +
Logging in, please wait a moment.
+ ); +} +``` + +TypeScript signature: + +```ts +function useLoggingIn(): boolean; +``` + +### useLoggingOut() + +Get a stateful value of whether the logout method is currently in progress. A hook. Uses `Meteor.loggingOut` (no online documentation), a reactive data source. + +- Arguments: *none*. +- Returns: `boolean`. + +Example: + +```tsx +import React from 'react'; +import { useLoggingOut } from 'meteor/mdg:react-meteor-accounts'; + +function Foo() { + const loggingOut = useLoggingOut(); + + if (!loggingOut) { + return null; + } + + return ( +
Logging out, please wait a moment.
+ ); +} +``` + +TypeScript signature: + +```ts +function useLoggingOut(): boolean; +``` + +### withUser(...) + +Return a wrapped version of the given component, where the component receives a stateful prop of the current user record, `user`. A higher-order component. Uses [`Meteor.user`](https://docs.meteor.com/api/accounts.html#Meteor-user), a reactive data source. + +- Arguments: + +| Argument | Type | Required | Description | +| --- | --- | --- | --- | +| Component | `React.ComponentType` | yes | A React component. | + +- Returns: `React.ForwardRefExoticComponent`. + +Examples: + +```tsx +import React from 'react'; +import { withUser } from 'meteor/mdg:react-meteor-accounts'; + +class Foo extends React.Component { + render() { + if (this.props.user === null) { + return

Log in

; + } + + return

Hello {this.props.user.username}

; + } +} + +const FooWithUser = withUser(Foo); +``` + +TypeScript signature: + +```ts +function withUser

(Component: React.ComponentType

): React.ForwardRefExoticComponent & Partial> & React.RefAttributes>; +``` + +### withUserId(...) + +Return a wrapped version of the given component, where the component receives a stateful prop of the current user id. A higher-order component. Uses [`Meteor.userId`](https://docs.meteor.com/api/accounts.html#Meteor-userId), a reactive data source. + +- Arguments: + +| Argument | Type | Required | Description | +| --- | --- | --- | --- | +| Component | `React.ComponentType` | yes | A React component. | + +- Returns: `React.ForwardRefExoticComponent`. + +Example: + +```tsx +import React from 'react'; +import { withUserId } from 'meteor/mdg:react-meteor-accounts'; + +class Foo extends React.Component { + render() { + return ( +

+

Account Details

+ {this.props.userId ? ( +

Your unique account id is {this.props.userId}.

+ ) : ( +

Log-in to view your account details.

+ )} +
+ ); + } +} + +const FooWithUserId = withUserId(Foo); +``` + +TypeScript signature: + +```ts +function withUserId

(Component: React.ComponentType

): React.ForwardRefExoticComponent & Partial> & React.RefAttributes>; +``` + +### withLoggingIn(...) + +Return a wrapped version of the given component, where the component receives a stateful prop of whether a login method (e.g. `loginWith`) is currently in progress. A higher-order component. Uses [`Meteor.loggingIn`](https://docs.meteor.com/api/accounts.html#Meteor-loggingIn), a reactive data source. + +- Arguments: + +| Argument | Type | Required | Description | +| --- | --- | --- | --- | +| Component | `React.ComponentType` | yes | A React component. | + +- Returns: `React.ForwardRefExoticComponent`. + +Example: + +```tsx +import React from 'react'; +import { withLoggingIn } from 'meteor/mdg:react-meteor-accounts'; + +class Foo extends React.Component { + render() { + if (!this.props.loggingIn) { + return null; + } + + return ( +

Logging in, please wait a moment.
+ ); + } +} + +const FooWithLoggingIn = withLoggingIn(Foo); +``` + +TypeScript signatures: + +```ts +function withLoggingIn

(Component: React.ComponentType

): React.ForwardRefExoticComponent & Partial> & React.RefAttributes>; +``` + +### withLoggingOut(...) + +Return a wrapped version of the given component, where the component receives a stateful prop of whether the logout method is currently in progress. A higher-order component. Uses [`Meteor.loggingOut`](https://docs.meteor.com/api/accounts.html#Meteor-loggingOut), a reactive data source. + +- Arguments: + +| Argument | Type | Required | Description | +| --- | --- | --- | --- | +| Component | `React.ComponentType` | yes | A React component. | + +- Returns: `React.ForwardRefExoticComponent`. + +Example: + +```tsx +import React from 'react'; +import { withLoggingOut } from 'meteor/mdg:react-meteor-accounts'; + +class Foo extends React.Component { + render() { + if (!this.props.loggingOut) { + return null; + } + + return ( +

Logging out, please wait a moment.
+ ); + } +} + +const FooWithLoggingOut = withLoggingOut(Foo); +``` + +TypeScript signature: + +```ts +function withLoggingOut

(Component: React.ComponentType

): React.ForwardRefExoticComponent & Partial> & React.RefAttributes>; +``` diff --git a/packages/react-meteor-accounts/index.tests.ts b/packages/react-meteor-accounts/index.tests.ts new file mode 100644 index 00000000..628be844 --- /dev/null +++ b/packages/react-meteor-accounts/index.tests.ts @@ -0,0 +1 @@ +import './react-accounts.tests.tsx'; diff --git a/packages/react-meteor-accounts/index.ts b/packages/react-meteor-accounts/index.ts new file mode 100644 index 00000000..a7312bdf --- /dev/null +++ b/packages/react-meteor-accounts/index.ts @@ -0,0 +1,21 @@ +import { Meteor } from 'meteor/meteor'; +import React from 'react'; + +if (Meteor.isDevelopment) { + // Custom check instead of `checkNpmVersions` to reduce prod bundle size (~8kb). + const v = React.version.split('.').map(val => parseInt(val)); + if (v[0] < 16 || (v[0] === 16 && v[1] < 8)) { + console.warn('react-accounts requires React version >= 16.8.'); + } +} + +export { + useUser, + useUserId, + useLoggingIn, + useLoggingOut, + withUser, + withUserId, + withLoggingIn, + withLoggingOut +} from './react-accounts'; diff --git a/packages/react-meteor-accounts/package-lock.json b/packages/react-meteor-accounts/package-lock.json new file mode 100644 index 00000000..da1779fa --- /dev/null +++ b/packages/react-meteor-accounts/package-lock.json @@ -0,0 +1,554 @@ +{ + "name": "meteor-react-accounts", + "requires": true, + "lockfileVersion": 1, + "dependencies": { + "@babel/code-frame": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", + "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", + "dev": true, + "requires": { + "@babel/highlight": "^7.10.4" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", + "dev": true + }, + "@babel/highlight": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", + "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.10.4", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + }, + "dependencies": { + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + } + } + }, + "@babel/runtime": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.12.1.tgz", + "integrity": "sha512-J5AIf3vPj3UwXaAzb5j1xM4WAQDX3EMgemF8rjCP3SoW09LfRKAXQKt6CoVYl230P6iWdRcBbnLDDdnqWxZSCA==", + "dev": true, + "requires": { + "regenerator-runtime": "^0.13.4" + } + }, + "@babel/runtime-corejs3": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.12.1.tgz", + "integrity": "sha512-umhPIcMrlBZ2aTWlWjUseW9LjQKxi1dpFlQS8DzsxB//5K+u6GLTC/JliPKHsd5kJVPIU6X/Hy0YvWOYPcMxBw==", + "dev": true, + "requires": { + "core-js-pure": "^3.0.0", + "regenerator-runtime": "^0.13.4" + } + }, + "@jest/types": { + "version": "26.6.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.0.tgz", + "integrity": "sha512-8pDeq/JVyAYw7jBGU83v8RMYAkdrRxLG3BGnAJuqaQAUd6GWBmND2uyl+awI88+hit48suLoLjNFtR+ZXxWaYg==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^15.0.0", + "chalk": "^4.0.0" + } + }, + "@testing-library/dom": { + "version": "7.26.3", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-7.26.3.tgz", + "integrity": "sha512-/1P6taENE/H12TofJaS3L1J28HnXx8ZFhc338+XPR5y1E3g5ttOgu86DsGnV9/n2iPrfJQVUZ8eiGYZGSxculw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.10.3", + "@types/aria-query": "^4.2.0", + "aria-query": "^4.2.2", + "chalk": "^4.1.0", + "dom-accessibility-api": "^0.5.1", + "lz-string": "^1.4.4", + "pretty-format": "^26.4.2" + } + }, + "@testing-library/react": { + "version": "10.4.9", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-10.4.9.tgz", + "integrity": "sha512-pHZKkqUy0tmiD81afs8xfiuseXfU/N7rAX3iKjeZYje86t9VaB0LrxYVa+OOsvkrveX5jCK3IjajVn2MbePvqA==", + "dev": true, + "requires": { + "@babel/runtime": "^7.10.3", + "@testing-library/dom": "^7.22.3" + } + }, + "@testing-library/react-hooks": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@testing-library/react-hooks/-/react-hooks-7.0.2.tgz", + "integrity": "sha512-dYxpz8u9m4q1TuzfcUApqi8iFfR6R0FaMbr2hjZJy1uC8z+bO/K4v8Gs9eogGKYQop7QsrBTFkv/BCF7MzD2Cg==", + "dev": true, + "requires": { + "@babel/runtime": "^7.12.5", + "@types/react": ">=16.9.0", + "@types/react-dom": ">=16.9.0", + "@types/react-test-renderer": ">=16.9.0", + "react-error-boundary": "^3.1.0" + }, + "dependencies": { + "@babel/runtime": { + "version": "7.16.5", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.16.5.tgz", + "integrity": "sha512-TXWihFIS3Pyv5hzR7j6ihmeLkZfrXGxAr5UfSl8CHf+6q/wpiYDkUau0czckpYG8QmnCIuPpdLtuA9VmuGGyMA==", + "dev": true, + "requires": { + "regenerator-runtime": "^0.13.4" + } + } + } + }, + "@tsconfig/recommended": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@tsconfig/recommended/-/recommended-1.0.1.tgz", + "integrity": "sha512-2xN+iGTbPBEzGSnVp/Hd64vKJCJWxsi9gfs88x4PPMyEjHJoA3o5BY9r5OLPHIZU2pAQxkSAsJFqn6itClP8mQ==", + "dev": true + }, + "@types/aria-query": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-4.2.0.tgz", + "integrity": "sha512-iIgQNzCm0v7QMhhe4Jjn9uRh+I6GoPmt03CbEtwx3ao8/EfoQcmgtqH4vQ5Db/lxiIGaWDv6nwvunuh0RyX0+A==", + "dev": true + }, + "@types/bson": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/bson/-/bson-4.0.3.tgz", + "integrity": "sha512-mVRvYnTOZJz3ccpxhr3wgxVmSeiYinW+zlzQz3SXWaJmD1DuL05Jeq7nKw3SnbKmbleW5qrLG5vdyWe/A9sXhw==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/connect": { + "version": "3.4.33", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.33.tgz", + "integrity": "sha512-2+FrkXY4zllzTNfJth7jOqEHC+enpLeGslEhpnTAkg21GkRrWV4SsAtqchtT4YS9/nODBU2/ZfsBY2X4J/dX7A==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/istanbul-lib-coverage": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz", + "integrity": "sha512-sz7iLqvVUg1gIedBOvlkxPlc8/uVzyS5OwGz1cKjXzkl3FpL3al0crU8YGU1WoHkxn0Wxbw5tyi6hvzJKNzFsw==", + "dev": true + }, + "@types/istanbul-lib-report": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "*" + } + }, + "@types/istanbul-reports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.0.tgz", + "integrity": "sha512-nwKNbvnwJ2/mndE9ItP/zc2TCzw6uuodnF4EHYWD+gCQDVBuRQL5UzbZD0/ezy1iKsFU2ZQiDqg4M9dN4+wZgA==", + "dev": true, + "requires": { + "@types/istanbul-lib-report": "*" + } + }, + "@types/meteor": { + "version": "1.4.60", + "resolved": "https://registry.npmjs.org/@types/meteor/-/meteor-1.4.60.tgz", + "integrity": "sha512-NsuIIKtGABovJHrE2H0+PUDlGTuvCL3UjX9fgxJOk43oRzmA+1FMOnGz4n1n9J6G6vbw9PumdWZOWTZkH/NnRw==", + "dev": true, + "requires": { + "@types/connect": "*", + "@types/mongodb": "*", + "@types/react": "*", + "@types/underscore": "*" + } + }, + "@types/mongodb": { + "version": "3.5.30", + "resolved": "https://registry.npmjs.org/@types/mongodb/-/mongodb-3.5.30.tgz", + "integrity": "sha512-aKqOERMTA78LF5It0DeBRzFa4rXJ2Kmr+/EEG5GblSs0q1wRf3DqKErvQmFE8sFwsh5SBPuTU4h9yYHJY4dIJA==", + "dev": true, + "requires": { + "@types/bson": "*", + "@types/node": "*" + } + }, + "@types/node": { + "version": "14.14.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.0.tgz", + "integrity": "sha512-BfbIHP9IapdupGhq/hc+jT5dyiBVZ2DdeC5WwJWQWDb0GijQlzUFAeIQn/2GtvZcd2HVUU7An8felIICFTC2qg==", + "dev": true + }, + "@types/prop-types": { + "version": "15.7.3", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.3.tgz", + "integrity": "sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==", + "dev": true + }, + "@types/react": { + "version": "16.9.53", + "resolved": "https://registry.npmjs.org/@types/react/-/react-16.9.53.tgz", + "integrity": "sha512-4nW60Sd4L7+WMXH1D6jCdVftuW7j4Za6zdp6tJ33Rqv0nk1ZAmQKML9ZLD4H0dehA3FZxXR/GM8gXplf82oNGw==", + "dev": true, + "requires": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + } + }, + "@types/react-dom": { + "version": "17.0.11", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.11.tgz", + "integrity": "sha512-f96K3k+24RaLGVu/Y2Ng3e1EbZ8/cVJvypZWd7cy0ofCBaf2lcM46xNhycMZ2xGwbBjRql7hOlZ+e2WlJ5MH3Q==", + "dev": true, + "requires": { + "@types/react": "*" + } + }, + "@types/react-test-renderer": { + "version": "17.0.1", + "resolved": "https://registry.npmjs.org/@types/react-test-renderer/-/react-test-renderer-17.0.1.tgz", + "integrity": "sha512-3Fi2O6Zzq/f3QR9dRnlnHso9bMl7weKCviFmfF6B4LS1Uat6Hkm15k0ZAQuDz+UBq6B3+g+NM6IT2nr5QgPzCw==", + "dev": true, + "requires": { + "@types/react": "*" + } + }, + "@types/underscore": { + "version": "1.10.24", + "resolved": "https://registry.npmjs.org/@types/underscore/-/underscore-1.10.24.tgz", + "integrity": "sha512-T3NQD8hXNW2sRsSbLNjF/aBo18MyJlbw0lSpQHB/eZZtScPdexN4HSa8cByYwTw9Wy7KuOFr81mlDQcQQaZ79w==", + "dev": true + }, + "@types/yargs": { + "version": "15.0.9", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.9.tgz", + "integrity": "sha512-HmU8SeIRhZCWcnRskCs36Q1Q00KBV6Cqh/ora8WN1+22dY07AZdn6Gel8QZ3t26XYPImtcL8WV/eqjhVmMEw4g==", + "dev": true, + "requires": { + "@types/yargs-parser": "*" + } + }, + "@types/yargs-parser": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-15.0.0.tgz", + "integrity": "sha512-FA/BWv8t8ZWJ+gEOnLLd8ygxH/2UFbAvgEonyfN6yWGLKc7zVjbpl2Y4CTjid9h2RfgPP6SEt6uHwEOply00yw==", + "dev": true + }, + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "aria-query": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-4.2.2.tgz", + "integrity": "sha512-o/HelwhuKpTj/frsOsbNLNgnNGVIFsVP/SW2BSF14gVl7kAfMOJ6/8wUAUvG1R1NHKrfG+2sHZTu0yauT1qBrA==", + "dev": true, + "requires": { + "@babel/runtime": "^7.10.2", + "@babel/runtime-corejs3": "^7.10.2" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "core-js-pure": { + "version": "3.6.5", + "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.6.5.tgz", + "integrity": "sha512-lacdXOimsiD0QyNf9BC/mxivNJ/ybBGJXQFKzRekp1WTHoVUWsUHEn+2T8GJAzzIhyOuXA+gOxCVN3l+5PLPUA==", + "dev": true + }, + "csstype": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.3.tgz", + "integrity": "sha512-jPl+wbWPOWJ7SXsWyqGRk3lGecbar0Cb0OvZF/r/ZU011R4YqiRehgkQ9p4eQfo9DSDLqLL3wHwfxeJiuIsNag==", + "dev": true + }, + "dom-accessibility-api": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.4.tgz", + "integrity": "sha512-TvrjBckDy2c6v6RLxPv5QXOnU+SmF9nBII5621Ve5fu6Z/BDrENurBEvlC1f44lKEUVqOpK4w9E5Idc5/EgkLQ==", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "requires": { + "js-tokens": "^3.0.0 || ^4.0.0" + } + }, + "lz-string": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.4.4.tgz", + "integrity": "sha1-wNjq82BZ9wV5bh40SBHPTEmNOiY=", + "dev": true + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "dev": true + }, + "pretty-format": { + "version": "26.6.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.6.0.tgz", + "integrity": "sha512-Uumr9URVB7bm6SbaByXtx+zGlS+0loDkFMHP0kHahMjmfCtmFY03iqd++5v3Ld6iB5TocVXlBN/T+DXMn9d4BA==", + "dev": true, + "requires": { + "@jest/types": "^26.6.0", + "ansi-regex": "^5.0.0", + "ansi-styles": "^4.0.0", + "react-is": "^16.12.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + } + } + }, + "prop-types": { + "version": "15.7.2", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", + "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==", + "dev": true, + "requires": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.8.1" + } + }, + "react": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react/-/react-16.13.1.tgz", + "integrity": "sha512-YMZQQq32xHLX0bz5Mnibv1/LHb3Sqzngu7xstSM+vrkE5Kzr9xE0yMByK5kMoTK30YVJE61WfbxIFFvfeDKT1w==", + "dev": true, + "requires": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1", + "prop-types": "^15.6.2" + } + }, + "react-dom": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.13.1.tgz", + "integrity": "sha512-81PIMmVLnCNLO/fFOQxdQkvEq/+Hfpv24XNJfpyZhTRfO0QcmQIF/PgCa1zCOj2w1hrn12MFLyaJ/G0+Mxtfag==", + "dev": true, + "requires": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1", + "prop-types": "^15.6.2", + "scheduler": "^0.19.1" + } + }, + "react-error-boundary": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-3.1.4.tgz", + "integrity": "sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA==", + "dev": true, + "requires": { + "@babel/runtime": "^7.12.5" + }, + "dependencies": { + "@babel/runtime": { + "version": "7.16.5", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.16.5.tgz", + "integrity": "sha512-TXWihFIS3Pyv5hzR7j6ihmeLkZfrXGxAr5UfSl8CHf+6q/wpiYDkUau0czckpYG8QmnCIuPpdLtuA9VmuGGyMA==", + "dev": true, + "requires": { + "regenerator-runtime": "^0.13.4" + } + } + } + }, + "react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true + }, + "react-test-renderer": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-16.13.1.tgz", + "integrity": "sha512-Sn2VRyOK2YJJldOqoh8Tn/lWQ+ZiKhyZTPtaO0Q6yNj+QDbmRkVFap6pZPy3YQk8DScRDfyqm/KxKYP9gCMRiQ==", + "dev": true, + "requires": { + "object-assign": "^4.1.1", + "prop-types": "^15.6.2", + "react-is": "^16.8.6", + "scheduler": "^0.19.1" + } + }, + "regenerator-runtime": { + "version": "0.13.7", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz", + "integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==", + "dev": true + }, + "scheduler": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.19.1.tgz", + "integrity": "sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA==", + "dev": true, + "requires": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1" + } + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "typescript": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.0.3.tgz", + "integrity": "sha512-tEu6DGxGgRJPb/mVPIZ48e69xCn2yRmCgYmDugAVwmJ6o+0u1RI18eO7E7WBTLYLaEVVOhwQmcdhQHweux/WPg==", + "dev": true + } + } +} diff --git a/packages/react-meteor-accounts/package.js b/packages/react-meteor-accounts/package.js new file mode 100644 index 00000000..151eafe5 --- /dev/null +++ b/packages/react-meteor-accounts/package.js @@ -0,0 +1,29 @@ +/* global Package */ + +Package.describe({ + name: 'mdg:react-meteor-accounts', + summary: 'React hooks and HOCs for reactively tracking Meteor Accounts data', + version: '1.0.0', + documentation: 'README.md', + git: 'https://github.com/meteor/react-packages', +}); + +Package.onUse((api) => { + api.versionsFrom(['1.10', '2.3']); + + api.use(['accounts-base', 'tracker', 'typescript']); + + api.mainModule('index.ts', ['client', 'server'], { lazy: true }); +}); + +Package.onTest((api) => { + api.use([ + 'accounts-base', + 'accounts-password', + 'tinytest', + 'tracker', + 'typescript', + ]); + + api.mainModule('index.tests.ts'); +}); diff --git a/packages/react-meteor-accounts/package.json b/packages/react-meteor-accounts/package.json new file mode 100644 index 00000000..4f38b650 --- /dev/null +++ b/packages/react-meteor-accounts/package.json @@ -0,0 +1,17 @@ +{ + "name": "meteor-react-accounts", + "scripts": { + "make-types": "npx typescript react-accounts.tsx --jsx preserve --declaration --emitDeclarationOnly --esModuleInterop --outDir types --strict" + }, + "devDependencies": { + "@testing-library/react": "^10.0.2", + "@testing-library/react-hooks": "^7.0.2", + "@tsconfig/recommended": "^1.0.1", + "@types/meteor": "^1.4.42", + "@types/react": "^16.9.34", + "react": "16.13.1", + "react-dom": "16.13.1", + "react-test-renderer": "16.13.1", + "typescript": "^4.0.3" + } +} diff --git a/packages/react-meteor-accounts/react-accounts.tests.tsx b/packages/react-meteor-accounts/react-accounts.tests.tsx new file mode 100644 index 00000000..834c8798 --- /dev/null +++ b/packages/react-meteor-accounts/react-accounts.tests.tsx @@ -0,0 +1,332 @@ +import { renderHook } from '@testing-library/react-hooks/dom'; +import { cleanup, render, waitFor } from "@testing-library/react"; +import { Accounts } from "meteor/accounts-base"; +import { Meteor } from "meteor/meteor"; +import { Tinytest } from "meteor/tinytest"; +import React from "react"; +import { + useLoggingIn, + useLoggingOut, + useUser, + useUserId, + withLoggingIn, + WithLoggingInProps, + withLoggingOut, + WithLoggingOutProps, + withUser, + withUserId, + WithUserIdProps, + WithUserProps, +} from "./react-accounts"; + +// Prepare method for clearing DB (doesn't need to be isomorphic). +if (Meteor.isServer) { + Meteor.methods({ + reset() { + Meteor.users.remove({}); + }, + }); +} + +if (Meteor.isClient) { + // fixture data + const username = "username"; + const password = "password"; + + // common test actions + async function login() { + await new Promise((resolve, reject) => { + Meteor.loginWithPassword(username, password, (error) => { + if (error) reject(error); + else resolve(); + }); + }); + } + async function logout() { + await new Promise((resolve, reject) => { + Meteor.logout((error) => { + if (error) reject(error); + else resolve(); + }); + }); + } + + // common test arrangements + async function beforeEach() { + // reset DB; must complete before creation to avoid potential overlap + await new Promise((resolve, reject) => { + Meteor.call("reset", (error, result) => { + if (error) reject(error); + else resolve(result); + }); + }); + // prepare sample user + await new Promise((resolve, reject) => { + Accounts.createUser({ username, password }, (error) => { + if (error) reject(error); + else resolve(); + }); + }); + // logout since `createUser` auto-logs-in + await logout(); + } + + // NOTE: each test body has three blocks: Arrange, Act, Assert. + + Tinytest.addAsync( + "Hooks - useUserId - has initial value of `null`", + async function (test, onComplete) { + await beforeEach(); + + const { result } = renderHook(() => useUserId()); + + test.isNull(result.current); + onComplete(); + } + ); + + Tinytest.addAsync( + "Hooks - useUserId - is reactive to login", + async function (test, onComplete) { + await beforeEach(); + + const { result, waitForNextUpdate } = renderHook(() => useUserId()); + // use `waitFor*` instead of `await`; mimics consumer usage + login(); + await waitForNextUpdate(); + + test.isNotNull(result.current); + onComplete(); + } + ); + + Tinytest.addAsync( + "Hooks - useUserId - is reactive to logout", + async function (test, onComplete) { + await beforeEach(); + await login(); + + const { result, waitForNextUpdate } = renderHook(() => useUserId()); + // use `waitFor*` instead of `await`; mimics consumer usage + logout(); + await waitForNextUpdate(); + + test.isNull(result.current); + onComplete(); + } + ); + + Tinytest.addAsync( + "Hooks - useLoggingIn - has initial value of `false`", + async function (test, onComplete) { + await beforeEach(); + + const { result } = renderHook(() => useLoggingIn()); + + test.isFalse(result.current); + onComplete(); + } + ); + + Tinytest.addAsync( + "Hooks - useLoggingIn - is reactive to login starting", + async function (test, onComplete) { + await beforeEach(); + + const { result, waitForNextUpdate } = renderHook(() => useLoggingIn()); + login(); + // first update will be while login strategy is in progress + await waitForNextUpdate(); + + test.isTrue(result.current); + onComplete(); + } + ); + + Tinytest.addAsync( + "Hooks - useLoggingIn - is reactive to login finishing", + async function (test, onComplete) { + await beforeEach(); + + const { result, waitForNextUpdate } = renderHook(() => useLoggingIn()); + login(); + await waitForNextUpdate(); + // second update will be after login strategy finishes + await waitForNextUpdate(); + + test.isFalse(result.current); + onComplete(); + } + ); + + Tinytest.addAsync( + "Hooks - useLoggingOut - has initial value of `false`", + async function (test, onComplete) { + await beforeEach(); + + const { result } = renderHook(() => useLoggingOut()); + + test.isFalse(result.current); + onComplete(); + } + ); + + Tinytest.addAsync( + "Hooks - useLoggingOut - is reactive to logout starting", + async function (test, onComplete) { + await beforeEach(); + + const { result, waitForNextUpdate } = renderHook(() => useLoggingOut()); + logout(); + // first update will be while logout is in progress + await waitForNextUpdate(); + + test.isTrue(result.current); + onComplete(); + } + ); + + Tinytest.addAsync( + "Hooks - useLoggingOut - is reactive to logout finishing", + async function (test, onComplete) { + await beforeEach(); + + const { result, waitForNextUpdate } = renderHook(() => useLoggingOut()); + logout(); + await waitForNextUpdate(); + // second update will be after logout finishes + await waitForNextUpdate(); + + test.isFalse(result.current); + onComplete(); + } + ); + + Tinytest.addAsync( + "Hooks - useUser - has initial value of `null`", + async function (test, onComplete) { + await beforeEach(); + + const { result } = renderHook(() => useUser()); + + test.isNull(result.current); + onComplete(); + } + ); + + Tinytest.addAsync( + "Hooks - useUser - is reactive to login", + async function (test, onComplete) { + await beforeEach(); + + const { result, waitForNextUpdate } = renderHook(() => useUser()); + // use `waitFor*` instead of `await`; mimics consumer usage + login(); + await waitForNextUpdate(); + + test.isNotNull(result.current); + test.equal( + result.current.username, + username, + "Expected username to match" + ); + onComplete(); + } + ); + + Tinytest.addAsync( + "Hooks - useUser - is reactive to logout", + async function (test, onComplete) { + await beforeEach(); + await login(); + + const { result, waitForNextUpdate } = renderHook(() => useUser()); + // use `waitFor*` instead of `await`; mimics consumer usage + logout(); + await waitForNextUpdate(); + + test.isNull(result.current); + onComplete(); + } + ); + + // Since the HOCs wrap with hooks, the logic is already tested in 'Hooks' tests, and we only really need to test for prop forwarding. However, doing so for the "non-initial" case of all these values seems more prudent than just checking the default of `null` or `false`. + + // :NOTE: these tests can be flaky (like 1 in 5 runs). + + Tinytest.addAsync( + "HOCs - withUserId - forwards reactive value", + async function (test, onComplete) { + await beforeEach(); + function Foo({ userId }: WithUserIdProps) { + // need something we can easily find; we don't know the id + return {Boolean(userId).toString()}; + } + const FooWithUserId = withUserId(Foo); + const { findByText } = render(); + + login(); + + await waitFor(() => findByText("true")); + cleanup(); + onComplete(); + } + ); + + // :TODO: this is flaky, fails ~1 in 10 + Tinytest.addAsync( + "HOCs - withUser - forwards reactive value", + async function (test, onComplete) { + await beforeEach(); + function Foo({ user }: WithUserProps) { + return {user?.username || String(user)}; + } + const FooWithUser = withUser(Foo); + const { findByText } = render(); + + login(); + + await waitFor(() => findByText(username)); + cleanup(); + onComplete(); + } + ); + + Tinytest.addAsync( + "HOCs - withLoggingIn - forwards reactive value", + async function (test, onComplete) { + await beforeEach(); + function Foo({ loggingIn }: WithLoggingInProps) { + return {loggingIn.toString()}; + } + const FooWithLoggingIn = withLoggingIn(Foo); + const { findByText } = render(); + + login(); + + await waitFor(() => findByText("true")); + cleanup(); + onComplete(); + } + ); + + // :TODO: this is flaky, fails ~1 in 5 + Tinytest.addAsync( + "HOCs - withLoggingOut - forwards reactive value", + async function (test, onComplete) { + await beforeEach(); + function Foo({ loggingOut }: WithLoggingOutProps) { + return {loggingOut.toString()}; + } + const FooWithLoggingOut = withLoggingOut(Foo); + const { findByText } = render(); + await login(); + + logout(); + + await waitFor(() => findByText("true")); + cleanup(); + onComplete(); + } + ); +} diff --git a/packages/react-meteor-accounts/react-accounts.tsx b/packages/react-meteor-accounts/react-accounts.tsx new file mode 100644 index 00000000..1e681f32 --- /dev/null +++ b/packages/react-meteor-accounts/react-accounts.tsx @@ -0,0 +1,162 @@ +import { Meteor } from 'meteor/meteor' +import { Tracker } from 'meteor/tracker' +import React, { useState, useEffect, forwardRef } from 'react' + +/** + * Hook to get a stateful value of the current user id. Uses `Meteor.userId`, a reactive data source. + * @see https://docs.meteor.com/api/accounts.html#Meteor-userId + */ +export function useUserId() { + const [userId, setUserId] = useState(Meteor.userId()) + useEffect(() => { + const computation = Tracker.autorun(() => { + setUserId(Meteor.userId()) + }) + return () => { + computation.stop() + } + }, []) + return userId +} + +export interface WithUserIdProps { + userId: string | null; +} + +/** + * HOC to forward a stateful value of the current user id. Uses `Meteor.userId`, a reactive data source. + * @see https://docs.meteor.com/api/accounts.html#Meteor-userId + */ +export function withUserId

(Component: React.ComponentType

) { + return forwardRef( + // Use `Omit` so instantiation doesn't require the prop. Union with `Partial` because prop should be optionally overridable / the wrapped component will be prepared for it anyways. + (props: Omit & Partial, ref) => { + const userId = useUserId(); + return ( + + ); + } + ); +} + +/** + * Hook to get a stateful value of the current user record. Uses `Meteor.user`, a reactive data source. + * @see https://docs.meteor.com/api/accounts.html#Meteor-user + */ +export function useUser() { + const [user, setUser] = useState(Meteor.user()); + useEffect(() => { + const computation = Tracker.autorun(() => { + let user = Meteor.user(); + // `Meteor.user` returns `undefined` after logout, but that ruins type signature and test parity. So, cast until that's fixed. + if (user === undefined) { + user = null; + } + setUser(user); + }); + return () => { + computation.stop(); + }; + }, []); + return user; +} + +export interface WithUserProps { + user: Meteor.User | null; +} + +/** + * HOC to get a stateful value of the current user record. Uses `Meteor.user`, a reactive data source. + * @see https://docs.meteor.com/api/accounts.html#Meteor-user + */ +export function withUser

(Component: React.ComponentType

) { + return forwardRef( + (props: Omit & Partial, ref) => { + const user = useUser(); + return ; + } + ); +} + +/** + * Hook to get a stateful value of whether a login method (e.g. `loginWith`) is currently in progress. Uses `Meteor.loggingIn`, a reactive data source. + * @see https://docs.meteor.com/api/accounts.html#Meteor-loggingIn + */ +export function useLoggingIn(): boolean { + const [loggingIn, setLoggingIn] = useState(Meteor.loggingIn()); + useEffect(() => { + const computation = Tracker.autorun(() => { + setLoggingIn(Meteor.loggingIn()); + }); + return () => { + computation.stop(); + }; + }, []); + return loggingIn; +} + +export interface WithLoggingInProps { + loggingIn: boolean; +} + +/** + * HOC to forward a stateful value of whether a login method (e.g. `loginWith`) is currently in progress. Uses `Meteor.loggingIn`, a reactive data source. + * @see https://docs.meteor.com/api/accounts.html#Meteor-loggingIn + */ +export function withLoggingIn

(Component: React.ComponentType

) { + return forwardRef( + ( + props: Omit & Partial, + ref + ) => { + const loggingIn = useLoggingIn(); + return ( + + ); + } + ); +} + +/** + * Hook to get a stateful value of whether the logout method is currently in progress. Uses `Meteor.loggingOut`, a reactive data source. + * @see https://docs.meteor.com/api/accounts.html#Meteor-loggingOut + */ +export function useLoggingOut(): boolean { + const [loggingOut, setLoggingOut] = useState(Meteor.loggingOut()); + useEffect(() => { + const computation = Tracker.autorun(() => { + setLoggingOut(Meteor.loggingOut()); + }); + return () => { + computation.stop(); + }; + }, []); + return loggingOut; +} + +export interface WithLoggingOutProps { + loggingOut: boolean; +} + +/** + * HOC to forward a stateful value of whether the logout method is currently in progress. Uses `Meteor.loggingOut`, a reactive data source. + * @see https://docs.meteor.com/api/accounts.html#Meteor-loggingOut + */ +export function withLoggingOut

(Component: React.ComponentType

) { + return forwardRef( + ( + props: Omit & Partial, + ref + ) => { + const loggingOut = useLoggingOut(); + return ( + + ); + } + ); +} diff --git a/packages/react-meteor-accounts/tsconfig.json b/packages/react-meteor-accounts/tsconfig.json new file mode 100644 index 00000000..1cb0c5fb --- /dev/null +++ b/packages/react-meteor-accounts/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "@tsconfig/recommended/tsconfig.json", + "compilerOptions": { + "jsx": "preserve" + } +} \ No newline at end of file diff --git a/packages/react-meteor-accounts/types/react-accounts.d.ts b/packages/react-meteor-accounts/types/react-accounts.d.ts new file mode 100644 index 00000000..bb5a5a84 --- /dev/null +++ b/packages/react-meteor-accounts/types/react-accounts.d.ts @@ -0,0 +1,59 @@ +import { Meteor } from 'meteor/meteor'; +import React from 'react'; +declare module 'meteor/meteor' { + module Meteor { + function loggingOut(): boolean; + } +} +/** + * Hook to get a stateful value of the current user id. Uses `Meteor.userId`, a reactive data source. + * @see https://docs.meteor.com/api/accounts.html#Meteor-userId + */ +export declare function useUserId(): string | null; +export interface WithUserIdProps { + userId: string | null; +} +/** + * HOC to forward a stateful value of the current user id. Uses `Meteor.userId`, a reactive data source. + * @see https://docs.meteor.com/api/accounts.html#Meteor-userId + */ +export declare function withUserId

(Component: React.ComponentType

): React.ForwardRefExoticComponent & Partial> & React.RefAttributes>; +/** + * Hook to get a stateful value of the current user record. Uses `Meteor.user`, a reactive data source. + * @see https://docs.meteor.com/api/accounts.html#Meteor-user + */ +export declare function useUser(): Meteor.User | null; +export interface WithUserProps { + user: Meteor.User | null; +} +/** + * HOC to get a stateful value of the current user record. Uses `Meteor.user`, a reactive data source. + * @see https://docs.meteor.com/api/accounts.html#Meteor-user + */ +export declare function withUser

(Component: React.ComponentType

): React.ForwardRefExoticComponent & Partial> & React.RefAttributes>; +/** + * Hook to get a stateful value of whether a login method (e.g. `loginWith`) is currently in progress. Uses `Meteor.loggingIn`, a reactive data source. + * @see https://docs.meteor.com/api/accounts.html#Meteor-loggingIn + */ +export declare function useLoggingIn(): boolean; +export interface WithLoggingInProps { + loggingIn: boolean; +} +/** + * HOC to forward a stateful value of whether a login method (e.g. `loginWith`) is currently in progress. Uses `Meteor.loggingIn`, a reactive data source. + * @see https://docs.meteor.com/api/accounts.html#Meteor-loggingIn + */ +export declare function withLoggingIn

(Component: React.ComponentType

): React.ForwardRefExoticComponent & Partial> & React.RefAttributes>; +/** + * Hook to get a stateful value of whether the logout method is currently in progress. Uses `Meteor.loggingOut`, a reactive data source. + * @see https://docs.meteor.com/api/accounts.html#Meteor-loggingOut + */ +export declare function useLoggingOut(): boolean; +export interface WithLoggingOutProps { + loggingOut: boolean; +} +/** + * HOC to forward a stateful value of whether the logout method is currently in progress. Uses `Meteor.loggingOut`, a reactive data source. + * @see https://docs.meteor.com/api/accounts.html#Meteor-loggingOut + */ +export declare function withLoggingOut

(Component: React.ComponentType

): React.ForwardRefExoticComponent & Partial> & React.RefAttributes>;