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
`) 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>;