Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: user experience improvements and e2e test coverage #321

Merged
merged 6 commits into from
Jan 16, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions examples/nextjs-app-router/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ router.

<!-- prettier-ignore-start -->
> [!WARNING]
> For convinience Ory provides a default "playground" project, that
> For convenience Ory provides a default "playground" project, that
> can be used to interact with Ory's APIs. It is a public project, that can be
> used by anyone and data can be deleted at any time. Make sure to use a
> dedicated project.
Expand All @@ -34,10 +34,20 @@ router.
The project files reside in the `app/` directory:

- `app/auth` - contains the page files for the user auth flows
- `app/settings` - contaisn the page file for the settings flow
- `app/settings` - contains the page file for the settings flow
- `app` - contains the root page file and layout.

## Need help?

If you have any issues using this examples, or Ory's products, don't hesitate to
reach out via the [Ory Community Slack](https://slack.ory.sh).

## Run against local Ory Network instance

This section is relevant to Ory engineers only. When running a local Ory Network
instance, you will need to disable TLS verification and set the
`NEXT_PUBLIC_ORY_SDK_URL` to `https://<slug>.projects.oryapis:8080`:

```sh
NODE_TLS_REJECT_UNAUTHORIZED=0 npm run dev
```
12 changes: 11 additions & 1 deletion examples/nextjs-pages-router/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ router.

<!-- prettier-ignore-start -->
> [!WARNING]
> For convinience Ory provides a default "playground" project, that
> For convenience Ory provides a default "playground" project, that
> can be used to interact with Ory's APIs. It is a public project, that can be
> used by anyone and data can be deleted at any time. Make sure to use a
> dedicated project.
Expand All @@ -33,3 +33,13 @@ router.

If you have any issues using this examples, or Ory's products, don't hesitate to
reach out via the [Ory Community Slack](https://slack.ory.sh).

## Run against local Ory Network instance

This section is relevant to Ory engineers only. When running a local Ory Network
instance, you will need to disable TLS verification and set the
`NEXT_PUBLIC_ORY_SDK_URL` to `https://<slug>.projects.oryapis:8080`:

```sh
NODE_TLS_REJECT_UNAUTHORIZED=0 npm run dev
```
25 changes: 11 additions & 14 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/elements-react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"dependencies": {
"@ory/client-fetch": "^1.15.12",
"@ory/client-fetch": "~1.16.1",
"@radix-ui/react-dropdown-menu": "2.1.2",
"class-variance-authority": "0.7.0",
"clsx": "2.1.1",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ function AuthMethodList({ options, setSelectedGroup }: AuthMethodListProps) {
const handleClick = (group: UiNodeGroupEnum) => {
if (isGroupImmediateSubmit(group)) {
// If the method is "immediate submit" (e.g. the method's submit button should be triggered immediately)
// then the methid needs to be added to the form data.
// then the method needs to be added to the form data.
setValue("method", group)
} else {
setSelectedGroup(group)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,20 @@ const wrapper = ({ children }: PropsWithChildren) => (
flow={
{
active: "code",
ui: { nodes: [], action: "", method: "" },
ui: {
nodes: [
{
group: "code",
attributes: {
node_type: "input",
name: "code",
type: "text",
},
},
],
action: "",
method: "",
},
} as unknown as LoginFlow // Fine, we're just testing the resolver
}
flowType={FlowType.Login}
Expand Down
26 changes: 25 additions & 1 deletion packages/elements-react/src/components/form/form-resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@

import { useOryFlow } from "../../context"
import { FormValues } from "../../types"
import { isUiNodeInputAttributes } from "@ory/client-fetch"

function isCodeResendRequest(data: FormValues) {
// There are two types of resend - one
return data.email ?? data.resend
}

Expand All @@ -20,10 +22,32 @@ export function useOryFormResolver() {

return (data: FormValues) => {
if (flowContainer.formState.current === "method_active") {
if (data.method === "code" && !data.code && !isCodeResendRequest(data)) {
// This is a workaround which prevents the flow from being submitted without a code,
// which in some cases can cause issues in Ory Kratos' resend detection.
if (
// When we submit a code
data.method === "code" &&
// And the code is not present
!data.code &&
// And the flow is not a code resend request
!isCodeResendRequest(data) &&
// And the flow has a code input node
flowContainer.flow.ui.nodes.find(({ attributes, group }) => {
if (!isUiNodeInputAttributes(attributes)) {
return false
}

return (
group === "code" &&
attributes.name === "code" &&
attributes.type !== "hidden"
)
})
) {
return {
values: data,
errors: {
// We know the code node exists, so we can safely hardcode the ID.
code: {
id: 4000002,
context: {
Expand Down
16 changes: 14 additions & 2 deletions packages/elements-react/src/components/form/nodes/input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,13 +79,25 @@ export const NodeInput = ({
}

return (
<Node.Button attributes={attrs} node={node} onClick={handleClick} />
<Node.Label
// The label is rendered in the button component
attributes={{ ...attrs, label: undefined }}
node={{ ...node, meta: { ...node.meta, label: undefined } }}
>
<Node.Button attributes={attrs} node={node} onClick={handleClick} />
</Node.Label>
)
case UiNodeInputAttributesTypeEnum.DatetimeLocal:
throw new Error("Not implemented")
case UiNodeInputAttributesTypeEnum.Checkbox:
return (
<Node.Checkbox attributes={attrs} node={node} onClick={handleClick} />
<Node.Label
// The label is rendered in the checkbox component
attributes={{ ...attrs, label: undefined }}
node={{ ...node, meta: { ...node.meta, label: undefined } }}
>
<Node.Checkbox attributes={attrs} node={node} onClick={handleClick} />
</Node.Label>
)
case UiNodeInputAttributesTypeEnum.Hidden:
return <Node.Input attributes={attrs} node={node} onClick={handleClick} />
Expand Down
7 changes: 7 additions & 0 deletions packages/elements-react/src/context/form-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { FlowType, UiNode, UiNodeGroupEnum } from "@ory/client-fetch"
import { useReducer } from "react"
import { isChoosingMethod } from "../components/card/card-two-step.utils"
import { OryFlowContainer } from "../util"
import { nodesToAuthMethodGroups } from "../util/ui"

export type FormState =
| { current: "provide_identifier" }
Expand Down Expand Up @@ -46,6 +47,12 @@ function parseStateFromFlow(flow: OryFlowContainer): FormState {
) {
return { current: "method_active", method: flow.flow.active }
} else if (isChoosingMethod(flow.flow.ui.nodes)) {
// Login has a special case where we only have one method. Here, we
// do not want to display the chooser.
const authMethods = nodesToAuthMethodGroups(flow.flow.ui.nodes)
if (authMethods.length === 1) {
return { current: "method_active", method: authMethods[0] }
}
Comment on lines +50 to +55
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This leads to this screen being shown if code is the only option for the user.

And in that case, we either need to auto submit the flow to then immediately show the code input, or do show the method selector so that the user can trigger the code sending.

image

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair point! I think auto-submit is the way to go. Ideally, this would be solved in kratos' identifier first

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea, actually I don't think we should work around this in Elements and just fix it in Kratos. So for now, let's show the method selector and fix it properly later in Kratos.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But only for the code method please, not for password etc.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return { current: "select_method" }
} else if (flow.flow.ui.messages?.some((m) => m.id === 1010016)) {
// Account linking edge case
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,7 @@ export function DefaultAuthMethodListItem({
className="flex cursor-pointer gap-3 py-2 text-left items-start"
onClick={onClick}
type={isGroupImmediateSubmit(group) ? "submit" : "button"}
id={`auth-method-list-item-${group}`}
data-testid="auth-method-list-item"
data-testid={`ory/ui/groups/auth-method/${group}`}
aria-label={`Authenticate with ${group}`}
>
<span className="mt-1">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,11 @@ import { FlowType, UiNode } from "@ory/client-fetch"
import { useOryFlow } from "@ory/elements-react"
import IconArrowLeft from "../../assets/icons/arrow-left.svg"
import { omit } from "../../utils/attributes"
import { restartFlowUrl } from "../../utils/url"

export function DefaultCurrentIdentifierButton() {
const {
flow: { ui },
flowType,
config,
formState,
} = useOryFlow()
const { flow, flowType, config, formState } = useOryFlow()
const ui = flow.ui

if (formState.current === "provide_identifier") {
return null
Expand All @@ -26,7 +23,12 @@ export function DefaultCurrentIdentifierButton() {
) {
return null
}
const initFlowUrl = `${config.sdk.url}/self-service/${flowType}/browser`

const initFlowUrl = restartFlowUrl(
flow,
`${config.sdk.url}/self-service/${flowType}/browser`,
)

const attributes = omit(nodeBackButton.attributes, [
"autocomplete",
"node_type",
Expand All @@ -41,6 +43,7 @@ export function DefaultCurrentIdentifierButton() {
{...attributes}
href={initFlowUrl}
title={`Adjust ${nodeBackButton?.attributes.value}`}
data-testid={"ory/ui/login/link/restart"}
jonas-jonas marked this conversation as resolved.
Show resolved Hide resolved
>
<span className="inline-flex min-h-5 items-center gap-2 overflow-hidden text-ellipsis">
<IconArrowLeft
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { FlowType, UiNode, UiNodeInputAttributes } from "@ory/client-fetch"
import { useOryFlow } from "@ory/elements-react"
import { useFormContext } from "react-hook-form"
import { useIntl } from "react-intl"
import { initFlowUrl } from "../../utils/url"

export function DefaultCardFooter() {
const { flowType } = useOryFlow()
Expand All @@ -22,15 +23,18 @@ export function DefaultCardFooter() {
}
}

function getReturnToQueryParam() {
export function getReturnToQueryParam(flow: { return_to?: string }) {
if (flow.return_to) {
return flow.return_to
}
if (typeof window !== "undefined") {
const searchParams = new URLSearchParams(window.location.search)
return searchParams.get("return_to")
}
}

function LoginCardFooter() {
const { config, formState } = useOryFlow()
const { config, formState, flow } = useOryFlow()
const intl = useIntl()

if (
Expand All @@ -42,12 +46,6 @@ function LoginCardFooter() {
return null
}

let registrationLink = `${config.sdk.url}/self-service/registration/browser`
const returnTo = getReturnToQueryParam()
if (returnTo) {
registrationLink += `?return_to=${returnTo}`
}

return (
<span className="font-normal leading-normal antialiased text-interface-foreground-default-primary">
{intl.formatMessage({
Expand All @@ -56,7 +54,8 @@ function LoginCardFooter() {
})}{" "}
<a
className="text-button-link-brand-brand transition-colors hover:text-button-link-brand-brand-hover underline"
href={registrationLink}
href={initFlowUrl(config.sdk.url, "registration", flow)}
data-testid={"ory/ui/login/link/registration"}
>
{intl.formatMessage({
id: "login.registration-button",
Expand Down Expand Up @@ -95,12 +94,6 @@ function RegistrationCardFooter() {
}
}

let loginLink = `${config.sdk.url}/self-service/login/browser`
const returnTo = getReturnToQueryParam()
if (returnTo) {
loginLink += `?return_to=${returnTo}`
}

return (
<span className="font-normal leading-normal antialiased">
{formState.current === "method_active" ? (
Expand All @@ -126,7 +119,8 @@ function RegistrationCardFooter() {
})}{" "}
<a
className="text-button-link-brand-brand transition-colors hover:text-button-link-brand-brand-hover underline"
href={loginLink}
href={initFlowUrl(config.sdk.url, "login", flow)}
data-testid={"ory/ui/login/link/login"}
>
{intl.formatMessage({
id: "registration.login-button",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,6 @@ export const DefaultButton = ({
value={value}
name={name}
type={type === "button" ? "button" : "submit"} // TODO
onSubmit={() => {
setValue(name, value)
}}
onClick={(e) => {
onClick?.(e)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export const DefaultInput = ({
maxLength={maxlength}
autoComplete={autocomplete}
placeholder={formattedLabel}
data-testid={`ory/ui/node/input/${name}`}
className={cn(
"antialiased rounded-forms border leading-tight transition-colors placeholder:h-[20px] placeholder:text-input-foreground-tertiary focus-visible:outline-none focus:ring-0",
"bg-input-background-default border-input-border-default text-input-foreground-primary",
Expand Down
Loading
Loading