diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index a2038049e..60c27b0c7 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -12,7 +12,7 @@ export const globalTypes = { { value: "light", icon: "circlehollow", title: "light" }, { value: "dark", icon: "circle", title: "dark" }, ], - showName: true, + name: true, }, }, } diff --git a/package-lock.json b/package-lock.json index b501fb1af..cfda2480e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ ], "devDependencies": { "@babel/core": "^7.18.10", - "@ory/client": "^0.2.0-alpha.48", + "@ory/client": "^0.2.0-alpha.60", "@ory/elements": "*", "@ory/elements-test": "*", "@ory/integrations": "^0.2.7", @@ -7700,9 +7700,9 @@ } }, "node_modules/@ory/client": { - "version": "0.2.0-alpha.48", - "resolved": "https://registry.npmjs.org/@ory/client/-/client-0.2.0-alpha.48.tgz", - "integrity": "sha512-iZ5N9XBD83PoZkmEamdWGG/9L3B+rw48uP1uLYsXV03M99GGIqqLKBvAJSk95sbCvWmsexxntrqmMb0CeAouwg==", + "version": "0.2.0-alpha.60", + "resolved": "https://registry.npmjs.org/@ory/client/-/client-0.2.0-alpha.60.tgz", + "integrity": "sha512-fGovJ/xIl7dvJJP9/IL4Xu1yiOCy9pvmkfj2xnHZbPrIbL9c9tqVcC3CSlzBq6zJQZMC3XI7VmZ8uEQ+cF4suw==", "dev": true, "dependencies": { "axios": "^0.21.4" @@ -37492,9 +37492,9 @@ } }, "@ory/client": { - "version": "0.2.0-alpha.48", - "resolved": "https://registry.npmjs.org/@ory/client/-/client-0.2.0-alpha.48.tgz", - "integrity": "sha512-iZ5N9XBD83PoZkmEamdWGG/9L3B+rw48uP1uLYsXV03M99GGIqqLKBvAJSk95sbCvWmsexxntrqmMb0CeAouwg==", + "version": "0.2.0-alpha.60", + "resolved": "https://registry.npmjs.org/@ory/client/-/client-0.2.0-alpha.60.tgz", + "integrity": "sha512-fGovJ/xIl7dvJJP9/IL4Xu1yiOCy9pvmkfj2xnHZbPrIbL9c9tqVcC3CSlzBq6zJQZMC3XI7VmZ8uEQ+cF4suw==", "dev": true, "requires": { "axios": "^0.21.4" diff --git a/package.json b/package.json index 206b310b8..276a8d389 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ }, "devDependencies": { "@babel/core": "^7.18.10", - "@ory/client": "^0.2.0-alpha.48", + "@ory/client": "^0.2.0-alpha.60", "@ory/elements": "*", "@ory/elements-test": "*", "@ory/integrations": "^0.2.7", diff --git a/src/markup-components/components.ts b/src/markup-components/components.ts index 8c18019e7..8d83840a2 100644 --- a/src/markup-components/components.ts +++ b/src/markup-components/components.ts @@ -48,8 +48,10 @@ import { UserSettingsCardProps, WebAuthnSettingsProps, WebAuthnSettingsSection as webAuthnSettingsSection, + UserConsentCard as userConsentCard, } from "../react-components" import { CodeBoxProps } from "../react-components/codebox" +import { ComponentProps } from "react" import { ComponentWrapper } from "./component-wrapper" export const ButtonLink = (props: ButtonLinkProps) => { @@ -154,6 +156,10 @@ export const LookupSecretSettingsSection = ( return ComponentWrapper(lookupSecretSettingsSection(props)) } +export const UserConsentCard = ( + props: ComponentProps, +) => ComponentWrapper(userConsentCard(props)) + export type { ButtonLinkProps, ButtonProps, diff --git a/src/react-components/ory/index.ts b/src/react-components/ory/index.ts index 533509372..991ba4bdc 100644 --- a/src/react-components/ory/index.ts +++ b/src/react-components/ory/index.ts @@ -20,3 +20,4 @@ export * from "./sections/webauthn-settings-section" export * from "./user-auth-card" export * from "./user-error-card" export * from "./user-settings-card" +export * from "./user-consent-card" diff --git a/src/react-components/ory/user-consent-card.spec.tsx b/src/react-components/ory/user-consent-card.spec.tsx new file mode 100644 index 000000000..6aeff741a --- /dev/null +++ b/src/react-components/ory/user-consent-card.spec.tsx @@ -0,0 +1,38 @@ +import { OAuth2ConsentRequest } from "@ory/client" +import { + AuthPage, + loginFixture, + recoveryFixture, + registrationFixture, + twoFactorLoginFixture, + verificationFixture, +} from "@ory/elements-test" +import { expect, test } from "@playwright/experimental-ct-react" +import { ComponentProps } from "react" +import { ConsentPage } from "../../test/ConsentPage" +import { UserConsentCard } from "./user-consent-card" + +test("ory consent card login flow", async ({ mount }) => { + const defaults = { + csrfToken: "csrfToken_example", + action: "consent", + client_name: "Best App", + client: { + tos_uri: "https://test_tos_uri/", + policy_uri: "https://test_policy_uri/", + }, + consent: {} as OAuth2ConsentRequest, + requested_scope: ["test_scope1", "test_scope2", "test_scope3"], + } + + const component = await mount() + + const consentComponent = new ConsentPage(component) + + await consentComponent.expectScopeFields(defaults.requested_scope) + await consentComponent.expectUris([ + "https://test_tos_uri/", + "https://test_policy_uri/", + ]) + await consentComponent.expectAllowSubmit() +}) diff --git a/src/react-components/ory/user-consent-card.tsx b/src/react-components/ory/user-consent-card.tsx new file mode 100644 index 000000000..7a19c046d --- /dev/null +++ b/src/react-components/ory/user-consent-card.tsx @@ -0,0 +1,112 @@ +import { gridStyle, typographyStyle } from "../../theme" +import { Button } from "../button" +import { ButtonLink } from "../button-link" +import { Card } from "../card" +import { Typography } from "../typography" + +import "../../assets/fontawesome.min.css" +import "../../assets/fa-solid.min.css" +import { Checkbox } from "../checkbox" +import { OAuth2Client, OAuth2ConsentRequest } from "@ory/client" +import { style } from "@vanilla-extract/css" +import { recipe } from "@vanilla-extract/recipes" +import { Divider } from "../divider" + +export type UserConsentCardProps = { + csrfToken: string + consent: OAuth2ConsentRequest + cardImage?: string | React.ReactElement + client_name: string + requested_scope?: string[] + client?: OAuth2Client + action: string +} + +export const UserConsentCard = ({ + csrfToken, + consent, + cardImage, + client_name = "Unnamed Client", + requested_scope = [], + client, + action, +}: UserConsentCardProps) => { + return ( + + {client_name} + + } + image={cardImage} + > +
+ + +
+
+ + The application requests access to the following permissions: + +
+
+ {requested_scope.map((scope) => ( + + ))} +
+
+ + Only grant permissions if you trust this site or app. You do not + need to accept all permissions. + +
+
+ {client?.policy_uri && ( + + Privacy Policy + + )} + {client?.tos_uri && ( + + Terms of Service + + )} +
+ +
+ + + Remember this decision for next time. The application will not be + able to ask for additional permissions without your consent. + +
+
+
+
+
+
+ ) +} diff --git a/src/stories/Ory/Consent.stories.tsx b/src/stories/Ory/Consent.stories.tsx new file mode 100644 index 000000000..e51612d28 --- /dev/null +++ b/src/stories/Ory/Consent.stories.tsx @@ -0,0 +1,26 @@ +import { ComponentProps } from "react" +import { ComponentMeta, Story } from "@storybook/react" + +import { Container } from "../storyhelper" +import { UserConsentCard } from "../../react-components" +import logo from "../assets/logo.svg" + +export default { + title: "Ory/UserConsentCard", + component: UserConsentCard, +} as ComponentMeta + +const Template: Story> = ( + args: ComponentProps, +) => ( + + + +) + +export const ConsentCard = Template.bind({}) +ConsentCard.args = { + cardImage: logo, + requested_scope: ["openid", "test_scope"], + client: { tos_uri: "example.com/", policy_uri: "example.com/" }, +} diff --git a/src/test/ConsentPage.ts b/src/test/ConsentPage.ts new file mode 100644 index 000000000..0087bc0d2 --- /dev/null +++ b/src/test/ConsentPage.ts @@ -0,0 +1,37 @@ +import { UiNode } from "@ory/client" +import { expect, Locator } from "@playwright/test" +import { Traits } from "./types" +import { inputNodesToRecord, isUiNode } from "./Utils" + +export class ConsentPage { + readonly locator: Locator + readonly formFields: Record = {} + + constructor(locator: Locator) { + this.locator = locator + } + + async expectScopeFields(scopes: string[]) { + for (const scope of scopes) { + await expect( + this.locator.locator(`input[type="checkbox"][value="${scope}"]`), + ).toBeVisible() + } + } + + async expectUris(uris: string[]) { + for (const uri of uris) { + await expect(this.locator.locator(`a[href="${uri}"]`)).toBeVisible() + } + } + + async expectAllowSubmit() { + await expect( + this.locator.locator('button[value="accept"][type="submit"]'), + ).toBeVisible() + } + + async submitForm() { + await this.locator.locator('[type="submit"]').click() + } +} diff --git a/src/theme/button.css.ts b/src/theme/button.css.ts index 2fe687a82..1bf1fbdde 100644 --- a/src/theme/button.css.ts +++ b/src/theme/button.css.ts @@ -60,6 +60,29 @@ export const buttonStyle = recipe({ fontWeight: 600, fontStyle: "normal", }, + outline: { + background: "none", + color: oryTheme.text.disabled, + ":hover": { + color: oryTheme.text.def, + }, + }, + error: { + background: "none", + color: oryTheme.error.muted, + ":hover": { + background: oryTheme.error.subtle, + }, + ":active": { + backgroundColor: oryTheme.error.emphasis, + color: oryTheme.error.def, + outline: "none", + }, + ":focus": { + background: "none", + color: oryTheme.error.def, + }, + }, }, }, }) diff --git a/src/theme/checkbox.css.ts b/src/theme/checkbox.css.ts index e06aea3b6..5c81002ac 100644 --- a/src/theme/checkbox.css.ts +++ b/src/theme/checkbox.css.ts @@ -24,15 +24,18 @@ export const checkboxInputStyle = style({ margin: pxToRem(3), color: oryTheme.accent.def, selectors: { + "&:checked": { + background: oryTheme.accent.def, + }, "&:checked::before": { fontFamily: "'Font Awesome 6 Free'", // this is required for the fontawesome icon to work - fontSize: pxToRem(10), + fontSize: pxToRem(13), display: "block", textAlign: "center", position: "relative", content: "\\f00c", // this is a fontawesome unicode character to switch back to a basic html checkmark use \\2713 - color: oryTheme.accent.def, - top: pxToRem(2.5), + color: "white", + fontWeight: "900", }, }, ":disabled": {