Skip to content
67 changes: 66 additions & 1 deletion docs/components/AuthorizationZone.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,14 @@ Input parameters for `FirestoreDocument` component is as follows:

`AuthorizationZone` component has `validator` property, which requires a type that is `(user: User | null) => Promise<boolean> | boolean`. This means it's a function that automatically is provided with an instance of [`User`][UserRefDoc] and might or might not be an async function (can return either `Promise<boolean>` or `boolean`).

You can write your own custom validator or use one of ready-made validators provided by `firereact`.
`firereact` already provides premade useful validators. Here is an exhaustive list of them:

- [`Validators.isAuthenticated`](#isauthenticated-validator)
- [`Validators.isAnonymous`](#isanonymous-validator)
- [`Validators.every`](#every-validator)
- [`Validators.some`](#some-validator)

You can also write your own custom validator.

### Custom Validator

Expand Down Expand Up @@ -192,6 +199,64 @@ Only takes positional parameters.
|---|---|---|---|---|
| `excludeFirebaseAnon` | `boolean` | Consider Firebase-handled anonymous as *authenticated* rather than *anonymous* | ❌ | `false` |

### `every` Validator

This validator is a kind of validator composer and will render the component only if all the subvalidators return `true`.

This example will render `onSuccess`:

```typescript
<AuthorizationZone
auth={auth}
validator={Validators.every([
// your own validators or premade validators
// can be async as well
() => true,
() => true,
])}
onSuccess={() => (
<p>successful</p>
)}
/>
```

#### Input Parameters

Only takes positional parameters.

Name | Type | Description | Required | Default Value |
|---|---|---|---|---|
| `validators` | An array of `(user: User | null) => Promise<boolean> | boolean` | Returns `true` if all validations pass. | ✅ | - |

### `some` Validator

This validator is a kind of validator composer and will render the component if any of subvalidators return `true`.

This example will render `onSuccess`:

```typescript
<AuthorizationZone
auth={auth}
validator={Validators.some([
// your own validators or premade validators
// can be async as well
() => false,
() => true,
])}
onSuccess={() => (
<p>successful</p>
)}
/>
```

#### Input Parameters

Only takes positional parameters.

Name | Type | Description | Required | Default Value |
|---|---|---|---|---|
| `validators` | An array of `(user: User | null) => Promise<boolean> | boolean` | Returns `true` if any of validations pass. | ✅ | - |

[AuthRefDoc]: https://firebase.google.com/docs/reference/node/firebase.auth.Auth
[UserRefDoc]: https://firebase.google.com/docs/reference/node/firebase.User
[onCreateTriggerDoc]: https://firebase.google.com/docs/functions/auth-events#trigger_a_function_on_user_creation
Expand Down
214 changes: 214 additions & 0 deletions src/auth/AuthorizationZone.comp.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -178,3 +178,217 @@ describe("Validators.isAnonymous", () => {
await deleteUser(credential.user);
});
});

describe("Validators.every", () => {
it("should return true if all async", async () => {
render(
<AuthorizationZone
auth={auth}
validator={Validators.every([
async () => true,
async () => true,
async () => true,
async () => true,
])}
onSuccess={() => <div>passed</div>}
onFailure={() => <div>failed</div>}
/>,
);
await sleep(50);
expect(screen.getByText("passed")).not.toBeUndefined();
});

it("should return false if all async", async () => {
render(
<AuthorizationZone
auth={auth}
validator={Validators.every([
async () => true,
async () => true,
async () => false,
async () => true,
])}
onSuccess={() => <div>passed</div>}
onFailure={() => <div>failed</div>}
/>,
);
expect(screen.getByText("failed")).not.toBeUndefined();
});

it("should return true if all non-async", async () => {
render(
<AuthorizationZone
auth={auth}
validator={Validators.every([
() => true,
() => true,
() => true,
() => true,
])}
onSuccess={() => <div>passed</div>}
onFailure={() => <div>failed</div>}
/>,
);
await sleep(50);
expect(screen.getByText("passed")).not.toBeUndefined();
});

it("should return false if all non-async", async () => {
render(
<AuthorizationZone
auth={auth}
validator={Validators.every([
() => true,
() => true,
() => false,
() => true,
])}
onSuccess={() => <div>passed</div>}
onFailure={() => <div>failed</div>}
/>,
);
expect(screen.getByText("failed")).not.toBeUndefined();
});

it("should return true if async and non-async", async () => {
render(
<AuthorizationZone
auth={auth}
validator={Validators.every([
async () => true,
() => true,
async () => true,
() => true,
])}
onSuccess={() => <div>passed</div>}
onFailure={() => <div>failed</div>}
/>,
);
await sleep(50);
expect(screen.getByText("passed")).not.toBeUndefined();
});

it("should return false if async and non-async", async () => {
render(
<AuthorizationZone
auth={auth}
validator={Validators.every([
async () => true,
() => true,
async () => false,
() => true,
])}
onSuccess={() => <div>passed</div>}
onFailure={() => <div>failed</div>}
/>,
);
expect(screen.getByText("failed")).not.toBeUndefined();
});
});

describe("Validators.some", () => {
it("should return true if all async", async () => {
render(
<AuthorizationZone
auth={auth}
validator={Validators.some([
async () => false,
async () => false,
async () => false,
async () => true,
])}
onSuccess={() => <div>passed</div>}
onFailure={() => <div>failed</div>}
/>,
);
await sleep(50);
expect(screen.getByText("passed")).not.toBeUndefined();
});

it("should return false if all async", async () => {
render(
<AuthorizationZone
auth={auth}
validator={Validators.some([
async () => false,
async () => false,
async () => false,
async () => false,
])}
onSuccess={() => <div>passed</div>}
onFailure={() => <div>failed</div>}
/>,
);
expect(screen.getByText("failed")).not.toBeUndefined();
});

it("should return true if all non-async", async () => {
render(
<AuthorizationZone
auth={auth}
validator={Validators.some([
() => false,
() => false,
() => true,
() => true,
])}
onSuccess={() => <div>passed</div>}
onFailure={() => <div>failed</div>}
/>,
);
await sleep(50);
expect(screen.getByText("passed")).not.toBeUndefined();
});

it("should return false if all non-async", async () => {
render(
<AuthorizationZone
auth={auth}
validator={Validators.some([
() => false,
() => false,
() => false,
() => false,
])}
onSuccess={() => <div>passed</div>}
onFailure={() => <div>failed</div>}
/>,
);
expect(screen.getByText("failed")).not.toBeUndefined();
});

it("should return true if async and non-async", async () => {
render(
<AuthorizationZone
auth={auth}
validator={Validators.some([
async () => false,
() => true,
async () => false,
() => false,
])}
onSuccess={() => <div>passed</div>}
onFailure={() => <div>failed</div>}
/>,
);
await sleep(50);
expect(screen.getByText("passed")).not.toBeUndefined();
});

it("should return false if async and non-async", async () => {
render(
<AuthorizationZone
auth={auth}
validator={Validators.some([
async () => false,
() => false,
async () => false,
() => false,
])}
onSuccess={() => <div>passed</div>}
onFailure={() => <div>failed</div>}
/>,
);
expect(screen.getByText("failed")).not.toBeUndefined();
});
});
18 changes: 18 additions & 0 deletions src/auth/AuthorizationZone.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,22 @@ export const Validators = {
(excludeFirebaseAnon = false) =>
(user: User | null) =>
user ? (excludeFirebaseAnon ? false : user.isAnonymous) : true,
every:
(validators: AuthorizationZoneValidator[]) =>
async (user: User | null): Promise<boolean> => {
const results = validators.map((v) => v(user));
const vals = results.filter((v) => typeof v === "boolean");
const tasks = results.filter((v) => v instanceof Promise);
const resolved = await Promise.all(tasks);
return [...vals, ...resolved].every((v) => v);
},
some:
(validators: AuthorizationZoneValidator[]) =>
async (user: User | null): Promise<boolean> => {
const results = validators.map((v) => v(user));
const vals = results.filter((v) => typeof v === "boolean");
const tasks = results.filter((v) => v instanceof Promise);
const resolved = await Promise.all(tasks);
return [...vals, ...resolved].some((v) => v);
},
};