Преглед изворни кода

Merge pull request #29972 from overleaf/td-ciam-onboarding

Unified access onboarding data collection pages

GitOrigin-RevId: c56a3d52f749883eeb2302e22aaf6bdf1239160c
Tim Down пре 10 месеци
родитељ
комит
452d854d5b
33 измењених фајлова са 1273 додато и 225 уклоњено
  1. 91 0
      services/web/app/views/_mixins/ciam_mixins.pug
  2. 26 21
      services/web/frontend/js/features/settings/components/emails/confirm-email-form.tsx
  3. 73 35
      services/web/frontend/js/features/settings/components/emails/downshift-input.tsx
  4. 35 0
      services/web/frontend/js/shared/components/ciam-stepper.tsx
  5. 3 1
      services/web/frontend/js/shared/components/ds/ds-button.tsx
  6. 27 0
      services/web/frontend/js/shared/components/ds/ds-form-checkbox.tsx
  7. 36 6
      services/web/frontend/js/shared/components/ds/ds-form-control.tsx
  8. 27 0
      services/web/frontend/js/shared/components/ds/ds-form-radio.tsx
  9. 28 0
      services/web/frontend/js/shared/components/ds/ds-notification.tsx
  10. 46 0
      services/web/frontend/js/shared/components/ds/ds-selection-group-item.tsx
  11. 20 0
      services/web/frontend/js/shared/components/ds/ds-selection-group.tsx
  12. 18 3
      services/web/frontend/js/shared/components/layouts/ciam-layout.tsx
  13. 82 36
      services/web/frontend/js/shared/components/select.tsx
  14. 6 0
      services/web/frontend/js/shared/hooks/use-is-ciam.ts
  15. 1 1
      services/web/frontend/stories/shared/ds/ds-button.stories.tsx
  16. 27 0
      services/web/frontend/stories/shared/ds/ds-form-check.stories.tsx
  17. 2 2
      services/web/frontend/stories/shared/ds/ds-form-control.stories.tsx
  18. 27 0
      services/web/frontend/stories/shared/ds/ds-form-radio.stories.tsx
  19. 32 0
      services/web/frontend/stories/shared/ds/ds-notification.stories.tsx
  20. 70 0
      services/web/frontend/stories/shared/ds/ds-selection-group.stories.tsx
  21. 1 1
      services/web/frontend/stories/shared/ds/ds-six-digits-input.stories.tsx
  22. 20 7
      services/web/frontend/stories/shared/select.stories.tsx
  23. 2 0
      services/web/frontend/stylesheets/ciam/all.scss
  24. 98 41
      services/web/frontend/stylesheets/ciam/ciam-layout.scss
  25. 31 0
      services/web/frontend/stylesheets/ciam/ciam-onboarding.scss
  26. 13 0
      services/web/frontend/stylesheets/ciam/ciam-try-premium.scss
  27. 152 68
      services/web/frontend/stylesheets/ds/colors.scss
  28. 2 0
      services/web/frontend/stylesheets/ds/components/all.scss
  29. 194 2
      services/web/frontend/stylesheets/ds/components/form-control.scss
  30. 30 0
      services/web/frontend/stylesheets/ds/components/notification.scss
  31. 40 0
      services/web/frontend/stylesheets/ds/components/selection-group.scss
  32. 10 1
      services/web/frontend/stylesheets/pages/onboarding-confirm-email-ciam.scss
  33. 3 0
      services/web/public/img/ciam-check-green.svg

+ 91 - 0
services/web/app/views/_mixins/ciam_mixins.pug

@@ -0,0 +1,91 @@
+include terms_of_service
+include recaptcha
+include ../../../modules/saas-authentication/app/views/_mixins
+
+mixin ciamLogo
+	header.ciam-logo
+		a.brand.overleaf-ds-logo.ciam-image-link(href='/')
+			span.visually-hidden Overleaf
+
+mixin ciamCardSeparator
+	hr.ciam-card-separator
+
+mixin ciamCardFooter
+	section.ciam-card-footer
+		+ciamCardSeparator
+		.ciam-footer-ds-logo
+			a.ciam-image-link(
+				href='https://www.digital-science.com/'
+				target='_blank'
+				rel='noopener noreferrer'
+			)
+				img(
+					src=buildImgPath('digital-science/digital-science.svg')
+					alt='Digital Science — home'
+				)
+		p
+			| !{translate('advancing_research_with', null, [{ name: 'a', attrs: { href: 'https://www.overleaf.com/', target: '_blank', rel: 'noopener noreferrer' }}, { name: 'a', attrs: { href: 'https://www.papersapp.com/', target: '_blank', rel: 'noopener noreferrer' }}])}
+
+mixin ciamTermsOfServiceAgreement
+	p
+		+termsOfServiceAgreementContent
+
+mixin ciamRecaptchaConditions
+	p
+		+recaptchaConditionsContent
+
+mixin ciamCustomFormDangerMessage(key)
+	div(
+		class='notification ciam-notification notification-type-error'
+		hidden
+		data-ol-custom-form-message=key
+		role='alert'
+		aria-live='polite'
+	)
+		.notification-icon
+			ph-warning-circle(aria-hidden='true')
+		.notification-content.text-left
+			block
+
+mixin ciamSamlErrorNotLoggedIn(error)
+	+samlErrorNotLoggedIn(error)
+		ph-warning-circle(aria-hidden='true')
+
+mixin ciamFooter
+	footer
+		.footer-links
+			a(href='https://www.overleaf.com/legal#Privacy') Privacy
+			a(href='https://www.overleaf.com/legal#Terms') Terms
+
+mixin ciamErrorNotification
+	.notification.notification-ds.notification-type-error(
+		role='alert'
+		aria-live='polite'
+	)
+		.notification-icon
+			ph-warning-circle(aria-hidden='true')
+		.notification-content-and-cta
+			.notification-content
+				block
+
+mixin ciamInfoNotification
+	.notification.notification-ds.notification-type-info(
+		role='alert'
+		aria-live='polite'
+	)
+		.notification-icon
+			ph-info(aria-hidden='true')
+		.notification-content-and-cta
+			.notification-content
+				block
+
+mixin ciamOrDivider
+	p.ciam-login-register-or-text-container= translate('login_register_or').toUpperCase()
+
+mixin ciamButtonContentLoading(loadingLabel)
+	span.button-content
+		span(data-ol-inflight='idle')
+			block
+		span.spinner-container(hidden data-ol-inflight='pending')
+			.loading-spinner-large.spinner-border.spinner-border-sm(aria-hidden='true')
+				span.visually-hidden= loadingLabel

+ 26 - 21
services/web/frontend/js/features/settings/components/emails/confirm-email-form.tsx

@@ -7,7 +7,6 @@ import {
   ComponentProps,
   FormEvent,
   MouseEventHandler,
-  useEffect,
   useState,
 } from 'react'
 import { Trans, useTranslation } from 'react-i18next'
@@ -21,6 +20,8 @@ import DSButton from '@/shared/components/ds/ds-button'
 import CIAMSixDigitsInput from '@/features/settings/components/emails/ciam-six-digits-input'
 import OLFormText from '@/shared/components/ol/ol-form-text'
 import DSFormText from '@/shared/components/ds/ds-form-text'
+import { CaretRight } from '@phosphor-icons/react'
+import DSNotification from '@/shared/components/ds/ds-notification'
 
 type Feedback = {
   type: 'input' | 'alert'
@@ -177,11 +178,11 @@ export function ConfirmEmailForm({
 
   if (successRedirectPath && successButtonText && successMessage) {
     return (
-      <ConfirmEmailSuccessfullForm
+      <ConfirmEmailSuccessfulForm
         successMessage={successMessage}
         successButtonText={successButtonText}
         redirectTo={successRedirectPath}
-        autoRedirect={isCiam ? 8000 : false}
+        isCiam={Boolean(isCiam)}
       />
     )
   }
@@ -196,6 +197,8 @@ export function ConfirmEmailForm({
   const SixDigits = isCiam ? CIAMSixDigitsInput : OLSixDigitsInput
   const FormText = isCiam ? DSFormText : OLFormText
 
+  const NotificationComponent = isCiam ? DSNotification : Notification
+
   return (
     <form
       onSubmit={submitHandler}
@@ -205,7 +208,7 @@ export function ConfirmEmailForm({
     >
       <div className="confirm-email-form-inner">
         {(feedback?.type === 'alert' || outerErrorDisplay) && (
-          <Notification
+          <NotificationComponent
             ariaLive="polite"
             className="confirm-email-alert"
             type={outerErrorDisplay ? 'error' : feedback!.style}
@@ -328,41 +331,43 @@ function Title({
   return <h5 className="h5">{t('confirm_your_email')}</h5>
 }
 
-function ConfirmEmailSuccessfullForm({
+function ConfirmEmailSuccessfulForm({
   successMessage,
   successButtonText,
   redirectTo,
-  autoRedirect = false,
+  isCiam,
 }: {
   successMessage: React.ReactNode
   successButtonText: string
   redirectTo: string
-  autoRedirect?: number | false
+  isCiam: boolean
 }) {
   const location = useLocation()
   const submitHandler = (e: FormEvent<HTMLFormElement>) => {
     e.preventDefault()
     location.assign(redirectTo)
   }
-
-  useEffect(() => {
-    if (autoRedirect) {
-      const timer = setTimeout(() => location.assign(redirectTo), autoRedirect)
-      return () => clearTimeout(timer)
-    }
-  }, [autoRedirect, location, redirectTo])
+  const button = isCiam ? (
+    <DSButton
+      type="submit"
+      variant="primary"
+      size="lg"
+      className="w-100"
+      trailingIcon={<CaretRight size={24} />}
+    >
+      {successButtonText}
+    </DSButton>
+  ) : (
+    <OLButton type="submit" variant="primary">
+      {successButtonText}
+    </OLButton>
+  )
 
   return (
     <form onSubmit={submitHandler} className="confirm-email-success-form">
       <div aria-live="polite">{successMessage}</div>
 
-      {!autoRedirect && (
-        <div className="form-actions">
-          <OLButton type="submit" variant="primary">
-            {successButtonText}
-          </OLButton>
-        </div>
-      )}
+      <div className="form-actions">{button}</div>
     </form>
   )
 }

+ 73 - 35
services/web/frontend/js/features/settings/components/emails/downshift-input.tsx

@@ -5,6 +5,9 @@ import { escapeRegExp } from 'lodash'
 import OLFormControl from '@/shared/components/ol/ol-form-control'
 import { DropdownItem } from '@/shared/components/dropdown/dropdown-menu'
 import OLFormLabel from '@/shared/components/ol/ol-form-label'
+import DSFormLabel from '@/shared/components/ds/ds-form-label'
+import DSFormControl from '@/shared/components/ds/ds-form-control'
+import { Check } from '@phosphor-icons/react'
 
 type DownshiftInputProps = {
   highlightMatches?: boolean
@@ -16,6 +19,7 @@ type DownshiftInputProps = {
   inputRef?: React.ForwardedRef<HTMLInputElement>
   showLabel?: boolean
   showSuggestedText?: boolean
+  isCiam?: boolean
 } & React.InputHTMLAttributes<HTMLInputElement>
 
 const filterItemsByInputValue = (
@@ -35,6 +39,7 @@ function Downshift({
   inputRef,
   showLabel = false,
   showSuggestedText = false,
+  isCiam = false,
 }: DownshiftInputProps) {
   const [inputItems, setInputItems] = useState(items)
 
@@ -76,7 +81,73 @@ function Downshift({
     )
   }
 
-  const shouldOpen = isOpen && inputItems.length
+  const TickIcon = function () {
+    return isCiam ? <Check /> : 'check'
+  }
+
+  const shouldOpen = isOpen && inputItems.length > 0
+
+  const dropdown = (
+    <ul
+      {...getMenuProps()}
+      className={classnames('dropdown-menu', 'select-dropdown-menu', {
+        show: shouldOpen,
+        'ciam-dropdown-menu': isCiam,
+      })}
+    >
+      {showSuggestedText && inputItems.length > 0 && (
+        <li>
+          <DropdownItem as="span" role={undefined} disabled>
+            {itemsTitle}
+          </DropdownItem>
+        </li>
+      )}
+      {inputItems.map((item, index) => (
+        // eslint-disable-next-line jsx-a11y/role-supports-aria-props
+        <li
+          key={`${item}${index}`}
+          {...getItemProps({ item, index })}
+          aria-selected={selectedItem === item}
+        >
+          <DropdownItem
+            as="span"
+            role={undefined}
+            className={classnames({
+              active: selectedItem === item,
+              'dropdown-item-highlighted': highlightedIndex === index,
+            })}
+            trailingIcon={selectedItem === item ? <TickIcon /> : undefined}
+          >
+            {highlightMatchedCharacters(item, inputValue)}
+          </DropdownItem>
+        </li>
+      ))}
+    </ul>
+  )
+
+  if (isCiam) {
+    return (
+      <div className="dropdown d-block">
+        <DSFormLabel
+          {...getLabelProps()}
+          className={showLabel ? '' : 'visually-hidden'}
+        >
+          {label}
+        </DSFormLabel>
+        <DSFormControl
+          {...getInputProps({
+            onChange: (event: React.ChangeEvent<HTMLInputElement>) => {
+              setValue(event.target.value)
+            },
+            ref: inputRef,
+          })}
+          placeholder={placeholder}
+          disabled={disabled}
+        />
+        {dropdown}
+      </div>
+    )
+  }
 
   return (
     <div className={classnames('dropdown', 'd-block')}>
@@ -98,40 +169,7 @@ function Downshift({
           disabled={disabled}
         />
       </div>
-      <ul
-        {...getMenuProps()}
-        className={classnames('dropdown-menu', 'select-dropdown-menu', {
-          show: shouldOpen,
-        })}
-      >
-        {showSuggestedText && inputItems.length && (
-          <li>
-            <DropdownItem as="span" role={undefined} disabled>
-              {itemsTitle}
-            </DropdownItem>
-          </li>
-        )}
-        {inputItems.map((item, index) => (
-          // eslint-disable-next-line jsx-a11y/role-supports-aria-props
-          <li
-            key={`${item}${index}`}
-            {...getItemProps({ item, index })}
-            aria-selected={selectedItem === item}
-          >
-            <DropdownItem
-              as="span"
-              role={undefined}
-              className={classnames({
-                active: selectedItem === item,
-                'dropdown-item-highlighted': highlightedIndex === index,
-              })}
-              trailingIcon={selectedItem === item ? 'check' : undefined}
-            >
-              {highlightMatchedCharacters(item, inputValue)}
-            </DropdownItem>
-          </li>
-        ))}
-      </ul>
+      {dropdown}
     </div>
   )
 }

+ 35 - 0
services/web/frontend/js/shared/components/ciam-stepper.tsx

@@ -0,0 +1,35 @@
+import classNames from 'classnames'
+import { useTranslation } from 'react-i18next'
+
+export function CiamStepper({
+  steps,
+  active,
+}: {
+  steps: number
+  active: number
+}) {
+  const { t } = useTranslation()
+  return (
+    <div className="ciam-stepper-container">
+      <div
+        className="ciam-stepper"
+        role="progressbar"
+        aria-label={t('progress_bar_percentage')}
+        aria-valuenow={active + 1}
+        aria-valuemax={steps}
+        tabIndex={0}
+      >
+        {Array.from({ length: steps }).map((_, i) => (
+          <div
+            key={i}
+            className={classNames({
+              step: true,
+              active: i === active,
+              completed: i < active,
+            })}
+          />
+        ))}
+      </div>
+    </div>
+  )
+}

+ 3 - 1
services/web/frontend/js/shared/components/ds/ds-button.tsx

@@ -6,6 +6,7 @@ import classNames from 'classnames'
 type DSButtonProps = Pick<
   ButtonProps,
   | 'children'
+  | 'className'
   | 'disabled'
   | 'href'
   | 'id'
@@ -32,6 +33,7 @@ const DSButton = forwardRef<HTMLButtonElement, DSButtonProps>(
   (
     {
       children,
+      className,
       leadingIcon,
       isLoading = false,
       loadingLabel,
@@ -44,7 +46,7 @@ const DSButton = forwardRef<HTMLButtonElement, DSButtonProps>(
   ) => {
     const { t } = useTranslation()
 
-    const buttonClassName = classNames('d-inline-grid btn-ds', {
+    const buttonClassName = classNames('d-inline-grid btn-ds', className, {
       'button-loading': isLoading,
     })
 

+ 27 - 0
services/web/frontend/js/shared/components/ds/ds-form-checkbox.tsx

@@ -0,0 +1,27 @@
+import { Form, FormCheckProps } from 'react-bootstrap'
+import classNames from 'classnames'
+
+type DSFormCheckboxProps = Pick<
+  FormCheckProps,
+  | 'disabled'
+  | 'checked'
+  | 'onChange'
+  | 'label'
+  | 'name'
+  | 'value'
+  | 'id'
+  | 'className'
+>
+
+function DSFormCheckbox(props: DSFormCheckboxProps) {
+  const { className, ...rest } = props
+  return (
+    <Form.Check
+      type="checkbox"
+      className={classNames('form-check-ds', className)}
+      {...rest}
+    />
+  )
+}
+
+export default DSFormCheckbox

+ 36 - 6
services/web/frontend/js/shared/components/ds/ds-form-control.tsx

@@ -1,18 +1,48 @@
-import { forwardRef } from 'react'
+import React, { forwardRef } from 'react'
 import { Form, FormControlProps } from 'react-bootstrap'
 import classnames from 'classnames'
 
-interface ButtonProps extends FormControlProps {
-  size?: 'lg'
+interface DSFormControlProps extends FormControlProps {
+  prepend?: React.ReactNode
+  append?: React.ReactNode
 }
 
-const DSFormControl = forwardRef<HTMLInputElement, ButtonProps>(
-  ({ className, ...props }, ref) => {
+const DSFormControl = forwardRef<HTMLInputElement, DSFormControlProps>(
+  ({ prepend, append, className, ...props }, ref) => {
+    if (prepend || append) {
+      const wrapperClassNames = classnames(
+        'form-control-wrapper-ds form-control-wrapper-lg-ds',
+        {
+          'form-control-wrapper-disabled-ds': props.disabled,
+        }
+      )
+
+      const formControlClassNames = classnames('form-control-ds', className, {
+        'form-control-offset-start-ds': prepend,
+        'form-control-offset-end-ds': append,
+      })
+
+      return (
+        <div className={wrapperClassNames}>
+          {prepend && (
+            <span className="form-control-start-icon-ds">{prepend}</span>
+          )}
+          <Form.Control
+            {...props}
+            className={formControlClassNames}
+            ref={ref}
+          />
+          {append && <span className="form-control-end-icon-ds">{append}</span>}
+        </div>
+      )
+    }
+
     return (
       <Form.Control
         ref={ref}
-        {...props}
+        size="lg"
         className={classnames('form-control-ds', className)}
+        {...props}
       />
     )
   }

+ 27 - 0
services/web/frontend/js/shared/components/ds/ds-form-radio.tsx

@@ -0,0 +1,27 @@
+import { Form, FormCheckProps } from 'react-bootstrap'
+import classNames from 'classnames'
+
+type DSFormRadioProps = Pick<
+  FormCheckProps,
+  | 'disabled'
+  | 'checked'
+  | 'onChange'
+  | 'label'
+  | 'name'
+  | 'value'
+  | 'id'
+  | 'className'
+>
+
+function DSFormRadio(props: DSFormRadioProps) {
+  const { className, ...rest } = props
+  return (
+    <Form.Check
+      type="radio"
+      className={classNames('form-check-ds', className)}
+      {...rest}
+    />
+  )
+}
+
+export default DSFormRadio

+ 28 - 0
services/web/frontend/js/shared/components/ds/ds-notification.tsx

@@ -0,0 +1,28 @@
+import Notification, {
+  NotificationProps,
+} from '@/shared/components/notification'
+import classNames from 'classnames'
+import { WarningCircle, Info } from '@phosphor-icons/react'
+
+type DSNotificationProps = Pick<
+  NotificationProps,
+  'content' | 'customIcon' | 'className'
+> & {
+  type: 'info' | 'error'
+}
+
+function DSNotification(props: DSNotificationProps) {
+  const { type, className, ...rest } = props
+  const customIcon = type === 'info' ? <Info /> : <WarningCircle />
+
+  return (
+    <Notification
+      type={type}
+      customIcon={customIcon}
+      className={classNames('notification-ds', className)}
+      {...rest}
+    />
+  )
+}
+
+export default DSNotification

+ 46 - 0
services/web/frontend/js/shared/components/ds/ds-selection-group-item.tsx

@@ -0,0 +1,46 @@
+import React from 'react'
+
+type DSSelectionGroupItemProps<ValueType> = {
+  checked?: boolean
+  disabled?: boolean
+  onChange?: (value: ValueType) => void
+  required?: boolean
+  children: React.ReactNode
+} & (
+  | {
+      type: 'radio'
+      name: string
+      value: ValueType
+    }
+  | {
+      type: 'checkbox'
+      name?: string
+      value?: ValueType
+    }
+)
+
+export default function DSSelectionGroupItem(
+  props: DSSelectionGroupItemProps<any>
+) {
+  const handleChange = () => {
+    props.onChange?.(props.value)
+  }
+
+  return (
+    <li className="selection-group-ds-item">
+      <label>
+        <input
+          type={props.type}
+          className="check-input-ds"
+          name={props.name}
+          value={props.value}
+          checked={props.checked}
+          disabled={props.disabled}
+          required={props.required}
+          onChange={handleChange}
+        />
+        {props.children}
+      </label>
+    </li>
+  )
+}

+ 20 - 0
services/web/frontend/js/shared/components/ds/ds-selection-group.tsx

@@ -0,0 +1,20 @@
+import classNames from 'classnames'
+
+export default function DSSelectionGroup({
+  legend,
+  className,
+  children,
+}: {
+  legend?: React.ReactNode
+  className?: string
+  children: React.ReactNode
+}) {
+  return (
+    <fieldset className={classNames('selection-group-ds', className)}>
+      {legend ? (
+        <legend className="selection-group-ds-legend">{legend}</legend>
+      ) : null}
+      <ul className="selection-group-ds-list">{children}</ul>
+    </fieldset>
+  )
+}

+ 18 - 3
services/web/frontend/js/shared/components/layouts/ciam-layout.tsx

@@ -17,16 +17,31 @@ const CiamLayout: FC<Props> = ({ children }: Props) => (
         <section className="ciam-card-footer">
           <hr className="ciam-card-separator" />
           <div className="ciam-footer-ds-logo">
-            <img src={dsLogo} alt="Digital Science — home" />
+            <a
+              href="https://www.digital-science.com/"
+              target="_blank"
+              rel="noopener noreferrer"
+              className="ciam-image-link"
+            >
+              <img src={dsLogo} alt="Digital Science — home" />
+            </a>
           </div>
           <p>
             <Trans
               i18nKey="advancing_research_with"
               components={[
                 // eslint-disable-next-line jsx-a11y/anchor-has-content,react/jsx-key
-                <a href="https://www.overleaf.com" />,
+                <a
+                  href="https://www.overleaf.com/"
+                  target="_blank"
+                  rel="noopener noreferrer"
+                />,
                 // eslint-disable-next-line jsx-a11y/anchor-has-content,react/jsx-key
-                <a href="https://www.papersapp.com/" />,
+                <a
+                  href="https://www.papersapp.com/"
+                  target="_blank"
+                  rel="noopener noreferrer"
+                />,
               ]}
             />
           </p>

+ 82 - 36
services/web/frontend/js/shared/components/select.tsx

@@ -12,8 +12,12 @@ import { useTranslation } from 'react-i18next'
 import { Form } from 'react-bootstrap'
 import FormControl from '@/shared/components/form/form-control'
 import MaterialIcon from '@/shared/components/material-icon'
+import { CaretUp, CaretDown, Check } from '@phosphor-icons/react'
 import { DropdownItem } from '@/shared/components/dropdown/dropdown-menu'
 import OLSpinner from './ol/ol-spinner'
+import DSFormLabel from '@/shared/components/ds/ds-form-label'
+import DSFormGroup from '@/shared/components/ds/ds-form-group'
+import DSFormControl from '@/shared/components/ds/ds-form-control'
 
 export type SelectProps<T> = {
   // The items rendered as dropdown options.
@@ -51,6 +55,8 @@ export type SelectProps<T> = {
   selectedIcon?: boolean
   // testId for the input element
   dataTestId?: string
+  // CIAM-specific layout
+  isCiam?: boolean
 }
 
 export const Select = <T,>({
@@ -70,6 +76,7 @@ export const Select = <T,>({
   loading = false,
   selectedIcon = false,
   dataTestId,
+  isCiam,
 }: SelectProps<T>) => {
   const [selectedItem, setSelectedItem] = useState<T | undefined | null>(
     defaultItem
@@ -145,6 +152,80 @@ export const Select = <T,>({
     value = defaultText
   }
 
+  const TickIcon = function () {
+    return isCiam ? <Check /> : 'check'
+  }
+
+  const dropdown = (
+    <ul
+      {...getMenuProps({ disabled })}
+      className={classNames('dropdown-menu', {
+        'w-100': !isCiam,
+        'ciam-dropdown-menu': isCiam,
+        show: isOpen,
+      })}
+    >
+      {isOpen &&
+        items?.map((item, index) => {
+          // We're using an actual disabled button so we don't need the
+          // aria-disabled prop
+          const { 'aria-disabled': disabled, ...itemProps } = getItemProps({
+            item,
+            index,
+          })
+          return (
+            <li role="none" key={itemToKey(item)}>
+              <DropdownItem
+                as="button"
+                type="button"
+                className={classNames({
+                  'select-highlighted': highlightedIndex === index,
+                })}
+                active={selectedItem === item}
+                trailingIcon={
+                  selectedIcon && selectedItem === item ? (
+                    <TickIcon />
+                  ) : undefined
+                }
+                description={itemToSubtitle ? itemToSubtitle(item) : undefined}
+                {...itemProps}
+                disabled={disabled}
+              >
+                {itemToString(item)}
+              </DropdownItem>
+            </li>
+          )
+        })}
+    </ul>
+  )
+
+  if (isCiam) {
+    return (
+      <div className="select-wrapper" ref={rootRef}>
+        <DSFormGroup>
+          {label ? (
+            <DSFormLabel {...getLabelProps()}>
+              {label} {optionalLabel && <span>({t('optional')})</span>}{' '}
+              {loading && <OLSpinner size="sm" />}
+            </DSFormLabel>
+          ) : null}
+          <DSFormControl
+            data-testid={dataTestId}
+            {...getToggleButtonProps({
+              disabled,
+              onKeyDown,
+              className: 'select-trigger',
+            })}
+            value={value}
+            readOnly
+            append={isOpen ? <CaretUp /> : <CaretDown />}
+          />
+          {dropdown}
+        </DSFormGroup>
+      </div>
+    )
+  }
+
   return (
     <div className="select-wrapper" ref={rootRef}>
       {label ? (
@@ -172,42 +253,7 @@ export const Select = <T,>({
           />
         }
       />
-      <ul
-        {...getMenuProps({ disabled })}
-        className={classNames('dropdown-menu w-100', { show: isOpen })}
-      >
-        {isOpen &&
-          items?.map((item, index) => {
-            // We're using an actual disabled button so we don't need the
-            // aria-disabled prop
-            const { 'aria-disabled': disabled, ...itemProps } = getItemProps({
-              item,
-              index,
-            })
-            return (
-              <li role="none" key={itemToKey(item)}>
-                <DropdownItem
-                  as="button"
-                  type="button"
-                  className={classNames({
-                    'select-highlighted': highlightedIndex === index,
-                  })}
-                  active={selectedItem === item}
-                  trailingIcon={
-                    selectedIcon && selectedItem === item ? 'check' : undefined
-                  }
-                  description={
-                    itemToSubtitle ? itemToSubtitle(item) : undefined
-                  }
-                  {...itemProps}
-                  disabled={disabled}
-                >
-                  {itemToString(item)}
-                </DropdownItem>
-              </li>
-            )
-          })}
-      </ul>
+      {dropdown}
     </div>
   )
 }

+ 6 - 0
services/web/frontend/js/shared/hooks/use-is-ciam.ts

@@ -0,0 +1,6 @@
+import { useSplitTestContext } from '@/shared/context/split-test-context'
+
+export default function useIsCiam() {
+  const { splitTestVariants } = useSplitTestContext()
+  return splitTestVariants.uniaccessphase1 === 'enabled'
+}

+ 1 - 1
services/web/frontend/stories/shared/ds-button.stories.tsx → services/web/frontend/stories/shared/ds/ds-button.stories.tsx

@@ -1,5 +1,5 @@
 import { Meta } from '@storybook/react'
-import { figmaDesignUrl } from '../../../.storybook/utils/figma-design-url'
+import { figmaDesignUrl } from '../../../../.storybook/utils/figma-design-url'
 import DSButton from '@/shared/components/ds/ds-button'
 
 type Args = React.ComponentProps<typeof DSButton>

+ 27 - 0
services/web/frontend/stories/shared/ds/ds-form-check.stories.tsx

@@ -0,0 +1,27 @@
+import type { Meta, StoryObj } from '@storybook/react'
+import DSFormCheckbox from '@/shared/components/ds/ds-form-checkbox'
+import { figmaDesignUrl } from '../../../../.storybook/utils/figma-design-url'
+
+const meta: Meta<typeof DSFormCheckbox> = {
+  title: 'Shared / DS Components / Form',
+  component: DSFormCheckbox,
+  parameters: {
+    controls: {
+      include: ['disabled'],
+    },
+  },
+}
+export default meta
+
+type Story = StoryObj<typeof DSFormCheckbox>
+
+export const Checkbox: Story = {
+  args: {
+    id: 'id-1',
+    label: 'Label',
+    disabled: false,
+  },
+  parameters: figmaDesignUrl(
+    'https://www.figma.com/design/aJQlecvqCS9Ry8b6JA1lQN/DS---Components?node-id=6135-5430&m=dev'
+  ),
+}

+ 2 - 2
services/web/frontend/stories/shared/ds-form-control.stories.tsx → services/web/frontend/stories/shared/ds/ds-form-control.stories.tsx

@@ -1,5 +1,5 @@
 import { Meta } from '@storybook/react'
-import { figmaDesignUrl } from '../../../.storybook/utils/figma-design-url'
+import { figmaDesignUrl } from '../../../../.storybook/utils/figma-design-url'
 import DSFormControl from '@/shared/components/ds/ds-form-control'
 import DSFormText from '@/shared/components/ds/ds-form-text'
 import DSFormGroup from '@/shared/components/ds/ds-form-group'
@@ -26,7 +26,7 @@ export const FormControl = ({ textType, value, ...args }: Args) => {
 }
 
 const meta: Meta<typeof FormControl> = {
-  title: 'Shared / DS Components',
+  title: 'Shared / DS Components / Form',
   component: FormControl,
   argTypes: {
     disabled: { control: 'boolean' },

+ 27 - 0
services/web/frontend/stories/shared/ds/ds-form-radio.stories.tsx

@@ -0,0 +1,27 @@
+import type { Meta, StoryObj } from '@storybook/react'
+import DSFormRadio from '@/shared/components/ds/ds-form-radio'
+import { figmaDesignUrl } from '../../../../.storybook/utils/figma-design-url'
+
+const meta: Meta<typeof DSFormRadio> = {
+  title: 'Shared / DS Components / Form',
+  component: DSFormRadio,
+  parameters: {
+    controls: {
+      include: ['disabled'],
+    },
+  },
+}
+export default meta
+
+type Story = StoryObj<typeof DSFormRadio>
+
+export const Radio: Story = {
+  args: {
+    id: 'id-1',
+    label: 'Label',
+    disabled: false,
+  },
+  parameters: figmaDesignUrl(
+    'https://www.figma.com/design/aJQlecvqCS9Ry8b6JA1lQN/DS---Components?node-id=6135-5510&m=dev'
+  ),
+}

+ 32 - 0
services/web/frontend/stories/shared/ds/ds-notification.stories.tsx

@@ -0,0 +1,32 @@
+import DSNotification from '@/shared/components/ds/ds-notification'
+
+type Args = React.ComponentProps<typeof DSNotification>
+
+export const Notification = (args: Args) => {
+  return <DSNotification {...args} />
+}
+
+export default {
+  title: 'Shared / DS Components / Notification',
+  component: DSNotification,
+  args: {
+    content: (
+      <p>
+        This can be <b>any HTML</b> passed to the component. For example,
+        paragraphs, headers, <code>code samples</code>,{' '}
+        <a href="/services/web/public">links</a>, etc are all supported.
+      </p>
+    ),
+  },
+  argTypes: {
+    type: {
+      control: 'radio',
+      options: ['info', 'error'],
+    },
+  },
+  parameters: {
+    controls: {
+      include: ['content', 'title', 'type'],
+    },
+  },
+}

+ 70 - 0
services/web/frontend/stories/shared/ds/ds-selection-group.stories.tsx

@@ -0,0 +1,70 @@
+import { Meta, type StoryObj } from '@storybook/react'
+import { figmaDesignUrl } from '../../../../.storybook/utils/figma-design-url'
+import DSSelectionGroup from '@/shared/components/ds/ds-selection-group'
+import DSSelectionGroupItem from '@/shared/components/ds/ds-selection-group-item'
+
+type Story = StoryObj<typeof DSSelectionGroup>
+
+export const SelectionGroupRadio: Story = {
+  args: {
+    legend: 'Select your favourite animal',
+  },
+  render: args => {
+    return (
+      <DSSelectionGroup {...args}>
+        <DSSelectionGroupItem type="radio" name="animal" value="wombat">
+          Wombat
+        </DSSelectionGroupItem>
+        <DSSelectionGroupItem type="radio" name="animal" value="capybara">
+          Capybara
+        </DSSelectionGroupItem>
+        <DSSelectionGroupItem type="radio" name="animal" value="badger">
+          Badger
+        </DSSelectionGroupItem>
+      </DSSelectionGroup>
+    )
+  },
+}
+
+export const SelectionGroupCheckbox: Story = {
+  args: {
+    legend: 'Which animals do you like?',
+  },
+  render: args => {
+    return (
+      <DSSelectionGroup {...args}>
+        <DSSelectionGroupItem type="checkbox" name="vole">
+          Vole
+        </DSSelectionGroupItem>
+        <DSSelectionGroupItem type="checkbox" name="spider">
+          Spider
+        </DSSelectionGroupItem>
+        <DSSelectionGroupItem type="checkbox" name="wombat">
+          Wombat
+        </DSSelectionGroupItem>
+        <DSSelectionGroupItem type="checkbox" name="capybara">
+          Capybara
+        </DSSelectionGroupItem>
+        <DSSelectionGroupItem type="checkbox" name="badger">
+          Badger
+        </DSSelectionGroupItem>
+      </DSSelectionGroup>
+    )
+  },
+}
+
+const meta: Meta<typeof DSSelectionGroup> = {
+  title: 'Shared / DS Components',
+  component: DSSelectionGroup,
+
+  parameters: {
+    controls: {
+      include: ['legend'],
+    },
+    ...figmaDesignUrl(
+      'https://www.figma.com/design/aJQlecvqCS9Ry8b6JA1lQN/DS---Components?node-id=6276-265&m=dev'
+    ),
+  },
+}
+
+export default meta

+ 1 - 1
services/web/frontend/stories/shared/ds-six-digits-input.stories.tsx → services/web/frontend/stories/shared/ds/ds-six-digits-input.stories.tsx

@@ -1,6 +1,6 @@
 import { ComponentProps, useEffect, useState } from 'react'
 import { Meta } from '@storybook/react'
-import { figmaDesignUrl } from '../../../.storybook/utils/figma-design-url'
+import { figmaDesignUrl } from '../../../../.storybook/utils/figma-design-url'
 import DSFormGroup from '@/shared/components/ds/ds-form-group'
 import DSFormLabel from '@/shared/components/ds/ds-form-label'
 import CIAMSixDigitsInput from '@/features/settings/components/emails/ciam-six-digits-input'

+ 20 - 7
services/web/frontend/stories/shared/select.stories.tsx

@@ -1,4 +1,10 @@
-import { Select } from '../../js/shared/components/select'
+import { Select } from '@/shared/components/select'
+import { Meta } from '@storybook/react'
+
+type Args = Pick<
+  React.ComponentProps<typeof Select>,
+  'disabled' | 'defaultText' | 'isCiam'
+>
 
 const items = [1, 2, 3, 4].map(index => ({
   key: index,
@@ -6,9 +12,10 @@ const items = [1, 2, 3, 4].map(index => ({
   group: index >= 3 ? 'Large numbers' : undefined,
 }))
 
-export const Base = () => {
+export const Base = (args: Args) => {
   return (
     <Select
+      {...args}
       items={items}
       itemToString={x => String(x?.value)}
       itemToKey={x => String(x.key)}
@@ -16,9 +23,10 @@ export const Base = () => {
   )
 }
 
-export const WithSubtitles = () => {
+export const WithSubtitles = (args: Args) => {
   return (
     <Select
+      {...args}
       items={items}
       itemToString={x => String(x?.value)}
       itemToKey={x => String(x.key)}
@@ -27,9 +35,10 @@ export const WithSubtitles = () => {
   )
 }
 
-export const WithSelectedIcon = () => {
+export const WithSelectedIcon = (args: Args) => {
   return (
     <Select
+      {...args}
       items={items}
       itemToString={x => String(x?.value)}
       itemToKey={x => String(x.key)}
@@ -39,9 +48,10 @@ export const WithSelectedIcon = () => {
   )
 }
 
-export const WithDisabledItem = () => {
+export const WithDisabledItem = (args: Args) => {
   return (
     <Select
+      {...args}
       items={items}
       itemToString={x => String(x?.value)}
       itemToKey={x => String(x.key)}
@@ -51,16 +61,19 @@ export const WithDisabledItem = () => {
   )
 }
 
-export default {
+const meta: Meta<typeof Select> = {
   title: 'Shared / Components / Select',
   component: Select,
   parameters: {
     controls: {
-      include: ['disabled', 'defaultText'],
+      include: ['disabled', 'defaultText', 'isCiam'],
     },
   },
   args: {
     disabled: false,
+    isCiam: false,
     defaultText: 'Choose an item',
   },
 }
+
+export default meta

+ 2 - 0
services/web/frontend/stylesheets/ciam/all.scss

@@ -4,3 +4,5 @@
 @import 'ciam-login';
 @import 'ciam-register';
 @import 'ciam-six-digits';
+@import 'ciam-try-premium';
+@import 'ciam-onboarding';

+ 98 - 41
services/web/frontend/stylesheets/ciam/ciam-layout.scss

@@ -1,5 +1,3 @@
-@use 'sass:math';
-
 @mixin ciam-bottom-margin {
   margin: 0 0 var(--ds-spacing-400);
 }
@@ -10,6 +8,15 @@
   gap: var(--ds-spacing-400);
 }
 
+@mixin ciam-h1 {
+  @include ds-heading-md-semibold;
+
+  font-family: var(--ds-font-family-sans), sans-serif;
+  color: var(--ds-color-text-primary);
+  padding-bottom: var(--ds-spacing-200);
+  margin: 0;
+}
+
 .overleaf-ds-logo {
   background-image: url('../../../frontend/js/shared/svgs/overleaf-a-ds-solution-mallard.svg');
 }
@@ -27,17 +34,11 @@
 
   --password-visibility-toggle-width: calc(24px + 2 * var(--ds-spacing-250));
 
-  &,
-  h1 {
-    font-family: var(--ds-font-family-sans), sans-serif;
-    color: var(--ds-color-text-primary);
-  }
+  font-family: var(--ds-font-family-sans), sans-serif;
+  color: var(--ds-color-text-primary);
 
   h1 {
-    @include ds-heading-md-semibold;
-
-    padding-bottom: var(--ds-spacing-200);
-    margin: 0;
+    @include ciam-h1;
   }
 
   footer {
@@ -112,35 +113,6 @@
   @include media-breakpoint-up(sm) {
     padding: var(--ds-spacing-1300);
   }
-
-  .notification {
-    @include ds-body-sm-regular;
-
-    color: var(--ds-color-text-primary);
-    padding: 0 var(--ds-spacing-400);
-    border-width: 0;
-    border-radius: var(--ds-border-radius-200);
-    gap: var(--ds-spacing-300);
-
-    .notification-icon {
-      font-size: math.div(20em, 14);
-      display: flex;
-      align-items: center;
-      padding: var(--ds-spacing-50);
-    }
-
-    .notification-content {
-      padding: var(--ds-spacing-400) 0;
-    }
-
-    &.notification-type-error {
-      background-color: var(--ds-color-red-50);
-    }
-
-    &.notification-type-info {
-      background-color: var(--ds-color-blue-50);
-    }
-  }
 }
 
 .ciam-login-register-error-container .notification {
@@ -189,9 +161,13 @@
 .ciam-footer-ds-logo {
   text-align: center;
   padding: var(--ds-spacing-200) 0;
+
+  a {
+    display: inline-block;
+  }
 }
 
-.ciam-stepper {
+.ciam-password-strength-bar {
   margin: 0;
   height: 4px;
   border-radius: var(--ds-border-radius-full);
@@ -225,3 +201,84 @@
     background-color: var(--ds-color-neutral-200);
   }
 }
+
+.ciam-list-style-check-green {
+  list-style: none;
+  padding: 0;
+  margin: 0;
+  display: flex;
+  flex-direction: column;
+  gap: var(--ds-spacing-200);
+
+  li {
+    background-image: url('../../../public/img/ciam-check-green.svg');
+    background-repeat: no-repeat;
+    background-position: left center;
+    background-size: 16px 16px;
+    padding-left: calc(16px + var(--ds-spacing-200));
+  }
+}
+
+.ciam-onboarding-steps-container {
+  text-align: center;
+  padding: var(--ds-spacing-250) var(--ds-spacing-400);
+}
+
+.ciam-dropdown-menu {
+  // Apply styling from DS design system to CIAM dropdowns
+  --dropdown-border-radius: var(--ds-border-radius-200);
+  --dropdown-text-active: var(--ds-color-text-primary);
+  --dropdown-background-active: var(--ds-color-neutral-50);
+  --dropdown-background-hover: var(--ds-color-neutral-50);
+  --bs-dropdown-item-padding-x: var(--ds-spacing-300);
+  --bs-dropdown-item-padding-y: var(--ds-spacing-250);
+
+  .dropdown-item {
+    border-radius: var(--ds-border-radius-200);
+  }
+
+  // Allow dropdowns to be wider than the form container where necessary
+  width: 100%;
+  overflow-x: hidden;
+
+  @include media-breakpoint-up(sm) {
+    width: auto;
+    min-width: 100%;
+    max-width: 640px;
+  }
+}
+
+.ciam-form-control-info {
+  @include ds-body-sm-regular;
+
+  padding-top: calc(var(--ds-spacing-200) - var(--ds-spacing-100));
+  margin: 0;
+  color: var(--ds-color-text-secondary);
+}
+
+.ciam-stepper-container {
+  padding: var(--ds-spacing-200) var(--ds-spacing-400) 0 var(--ds-spacing-400);
+}
+
+.ciam-stepper {
+  display: flex;
+  justify-content: center;
+  gap: var(--ds-spacing-400);
+
+  &:focus-visible {
+    @include ds-focus-outline;
+  }
+
+  .step {
+    display: inline-block;
+    border-radius: 50%;
+    background-color: var(--ds-color-text-disabled);
+    width: 8px;
+    height: 8px;
+
+    &.active,
+    &.completed {
+      background-color: var(--ds-color-text-primary);
+    }
+  }
+}

+ 31 - 0
services/web/frontend/stylesheets/ciam/ciam-onboarding.scss

@@ -0,0 +1,31 @@
+@import 'ds-design-system';
+
+.btn.btn-ds.ciam-onboarding-back-button {
+  --bs-btn-padding-x: 0;
+  --bs-btn-hover-bg: transparent;
+}
+
+.ciam-onboarding-email-updates p {
+  margin: 0;
+}
+
+.ciam-onboarding-privacy-note {
+  @include ds-body-sm-regular;
+
+  color: var(--ds-color-text-secondary);
+  padding-top: var(--ds-spacing-250);
+}
+
+.ciam-onboarding-step {
+  @include ciam-vertically-spaced;
+
+  padding-bottom: var(--ds-spacing-400);
+
+  legend {
+    @include ciam-h1;
+  }
+}
+
+.ciam-used-latex-options {
+  padding-bottom: var(--ds-spacing-200);
+}

+ 13 - 0
services/web/frontend/stylesheets/ciam/ciam-try-premium.scss

@@ -0,0 +1,13 @@
+@import 'ds-design-system';
+
+.ciam-try-premium-logo-container {
+  display: flex;
+  justify-content: center;
+  gap: var(--ds-spacing-600);
+  padding: var(--ds-spacing-200) 0 var(--ds-spacing-500) 0;
+}
+
+.ciam-try-premium-third-party-logo {
+  width: 36px;
+  height: 36px;
+}

+ 152 - 68
services/web/frontend/stylesheets/ds/colors.scss

@@ -1,83 +1,167 @@
+// Sass variables
+// Neutral/grey
+$ds-color-neutral-50: #fafafa;
+$ds-color-neutral-100: #f2f2f2;
+$ds-color-neutral-200: #e6e6e6;
+$ds-color-neutral-300: #d6d6d6;
+$ds-color-neutral-400: #c7c7c7;
+$ds-color-neutral-500: #b5b5b5;
+$ds-color-neutral-600: #a1a1a1;
+$ds-color-neutral-700: #8a8a8a;
+$ds-color-neutral-800: #6b6b6b;
+$ds-color-neutral-900: #383838;
+$ds-color-neutral-950: #262626;
+
+// Green
+$ds-color-green-50: #f2f8f5;
+$ds-color-green-100: #e3f2eb;
+$ds-color-green-200: #c0e7d6;
+$ds-color-green-300: #8adbb7;
+$ds-color-green-400: #38cc89;
+$ds-color-green-500: #26b072;
+$ds-color-green-600: #158954;
+$ds-color-green-700: #19754c;
+$ds-color-green-800: #196241;
+$ds-color-green-900: #164630;
+$ds-color-green-950: #112c20;
+
+// Yellow
+$ds-color-yellow-50: #fffaeb;
+$ds-color-yellow-100: #fff7db;
+$ds-color-yellow-200: #ffeeb8;
+$ds-color-yellow-300: #ffe58f;
+$ds-color-yellow-400: #ffda61;
+$ds-color-yellow-500: #ffcc20;
+$ds-color-yellow-600: #f0b800;
+$ds-color-yellow-700: #d1a000;
+$ds-color-yellow-800: #ad8500;
+$ds-color-yellow-900: #806200;
+$ds-color-yellow-950: #574200;
+
+// Red
+$ds-color-red-50: #fff5f7;
+$ds-color-red-100: #fee7eb;
+$ds-color-red-200: #fdc9d3;
+$ds-color-red-300: #fba7b7;
+$ds-color-red-400: #f97b92;
+$ds-color-red-500: #f51d43;
+$ds-color-red-600: #e60a32;
+$ds-color-red-700: #c3092b;
+$ds-color-red-800: #a10723;
+$ds-color-red-900: #75051a;
+$ds-color-red-950: #530412;
+
+// Blue
+$ds-color-blue-50: #f7fafd;
+$ds-color-blue-100: #ecf2f9;
+$ds-color-blue-200: #d4e3f2;
+$ds-color-blue-300: #bdd4ea;
+$ds-color-blue-400: #a2c3e2;
+$ds-color-blue-500: #7facd7;
+$ds-color-blue-600: #5893cb;
+$ds-color-blue-700: #3470a8;
+$ds-color-blue-800: #2b5d8c;
+$ds-color-blue-900: #1e4161;
+$ds-color-blue-950: #17314a;
+
+// Teal
+$ds-color-teal-50: #f1f8f8;
+$ds-color-teal-100: #e3f2f1;
+$ds-color-teal-200: #c1e2df;
+$ds-color-teal-300: #9ed1cd;
+$ds-color-teal-400: #6dbab4;
+$ds-color-teal-500: #4a9d96;
+$ds-color-teal-600: #438e88;
+$ds-color-teal-700: #3b7d77;
+$ds-color-teal-800: #2f6460;
+$ds-color-teal-900: #244c49;
+$ds-color-teal-950: #193432;
+
+// Black and white
+$ds-color-white: #fff;
+$ds-color-black: #000;
+
+// CSS custom properties
 :root {
   // Neutral/grey
-  --ds-color-neutral-50: #fafafa;
-  --ds-color-neutral-100: #f2f2f2;
-  --ds-color-neutral-200: #e6e6e6;
-  --ds-color-neutral-300: #d6d6d6;
-  --ds-color-neutral-400: #c7c7c7;
-  --ds-color-neutral-500: #b5b5b5;
-  --ds-color-neutral-600: #a1a1a1;
-  --ds-color-neutral-700: #8a8a8a;
-  --ds-color-neutral-800: #6b6b6b;
-  --ds-color-neutral-900: #383838;
-  --ds-color-neutral-950: #262626;
+  --ds-color-neutral-50: #{$ds-color-neutral-50};
+  --ds-color-neutral-100: #{$ds-color-neutral-100};
+  --ds-color-neutral-200: #{$ds-color-neutral-200};
+  --ds-color-neutral-300: #{$ds-color-neutral-300};
+  --ds-color-neutral-400: #{$ds-color-neutral-400};
+  --ds-color-neutral-500: #{$ds-color-neutral-500};
+  --ds-color-neutral-600: #{$ds-color-neutral-600};
+  --ds-color-neutral-700: #{$ds-color-neutral-700};
+  --ds-color-neutral-800: #{$ds-color-neutral-800};
+  --ds-color-neutral-900: #{$ds-color-neutral-900};
+  --ds-color-neutral-950: #{$ds-color-neutral-950};
 
   // Green
-  --ds-color-green-50: #f2f8f5;
-  --ds-color-green-100: #e3f2eb;
-  --ds-color-green-200: #c0e7d6;
-  --ds-color-green-300: #8adbb7;
-  --ds-color-green-400: #38cc89;
-  --ds-color-green-500: #26b072;
-  --ds-color-green-600: #158954;
-  --ds-color-green-700: #19754c;
-  --ds-color-green-800: #196241;
-  --ds-color-green-900: #164630;
-  --ds-color-green-950: #112c20;
+  --ds-color-green-50: #{$ds-color-green-50};
+  --ds-color-green-100: #{$ds-color-green-100};
+  --ds-color-green-200: #{$ds-color-green-200};
+  --ds-color-green-300: #{$ds-color-green-300};
+  --ds-color-green-400: #{$ds-color-green-400};
+  --ds-color-green-500: #{$ds-color-green-500};
+  --ds-color-green-600: #{$ds-color-green-600};
+  --ds-color-green-700: #{$ds-color-green-700};
+  --ds-color-green-800: #{$ds-color-green-800};
+  --ds-color-green-900: #{$ds-color-green-900};
+  --ds-color-green-950: #{$ds-color-green-950};
 
   // Yellow
-  --ds-color-yellow-50: #fffaeb;
-  --ds-color-yellow-100: #fff7db;
-  --ds-color-yellow-200: #ffeeb8;
-  --ds-color-yellow-300: #ffe58f;
-  --ds-color-yellow-400: #ffda61;
-  --ds-color-yellow-500: #ffcc20;
-  --ds-color-yellow-600: #f0b800;
-  --ds-color-yellow-700: #d1a000;
-  --ds-color-yellow-800: #ad8500;
-  --ds-color-yellow-900: #806200;
-  --ds-color-yellow-950: #574200;
+  --ds-color-yellow-50: #{$ds-color-yellow-50};
+  --ds-color-yellow-100: #{$ds-color-yellow-100};
+  --ds-color-yellow-200: #{$ds-color-yellow-200};
+  --ds-color-yellow-300: #{$ds-color-yellow-300};
+  --ds-color-yellow-400: #{$ds-color-yellow-400};
+  --ds-color-yellow-500: #{$ds-color-yellow-500};
+  --ds-color-yellow-600: #{$ds-color-yellow-600};
+  --ds-color-yellow-700: #{$ds-color-yellow-700};
+  --ds-color-yellow-800: #{$ds-color-yellow-800};
+  --ds-color-yellow-900: #{$ds-color-yellow-900};
+  --ds-color-yellow-950: #{$ds-color-yellow-950};
 
   // Red
-  --ds-color-red-50: #fff5f7;
-  --ds-color-red-100: #fee7eb;
-  --ds-color-red-200: #fdc9d3;
-  --ds-color-red-300: #fba7b7;
-  --ds-color-red-400: #f97b92;
-  --ds-color-red-500: #f51d43;
-  --ds-color-red-600: #e60a32;
-  --ds-color-red-700: #c3092b;
-  --ds-color-red-800: #a10723;
-  --ds-color-red-900: #75051a;
-  --ds-color-red-950: #530412;
+  --ds-color-red-50: #{$ds-color-red-50};
+  --ds-color-red-100: #{$ds-color-red-100};
+  --ds-color-red-200: #{$ds-color-red-200};
+  --ds-color-red-300: #{$ds-color-red-300};
+  --ds-color-red-400: #{$ds-color-red-400};
+  --ds-color-red-500: #{$ds-color-red-500};
+  --ds-color-red-600: #{$ds-color-red-600};
+  --ds-color-red-700: #{$ds-color-red-700};
+  --ds-color-red-800: #{$ds-color-red-800};
+  --ds-color-red-900: #{$ds-color-red-900};
+  --ds-color-red-950: #{$ds-color-red-950};
 
   // Blue
-  --ds-color-blue-50: #f7fafd;
-  --ds-color-blue-100: #ecf2f9;
-  --ds-color-blue-200: #d4e3f2;
-  --ds-color-blue-300: #bdd4ea;
-  --ds-color-blue-400: #a2c3e2;
-  --ds-color-blue-500: #7facd7;
-  --ds-color-blue-600: #5893cb;
-  --ds-color-blue-700: #3470a8;
-  --ds-color-blue-800: #2b5d8c;
-  --ds-color-blue-900: #1e4161;
-  --ds-color-blue-950: #17314a;
+  --ds-color-blue-50: #{$ds-color-blue-50};
+  --ds-color-blue-100: #{$ds-color-blue-100};
+  --ds-color-blue-200: #{$ds-color-blue-200};
+  --ds-color-blue-300: #{$ds-color-blue-300};
+  --ds-color-blue-400: #{$ds-color-blue-400};
+  --ds-color-blue-500: #{$ds-color-blue-500};
+  --ds-color-blue-600: #{$ds-color-blue-600};
+  --ds-color-blue-700: #{$ds-color-blue-700};
+  --ds-color-blue-800: #{$ds-color-blue-800};
+  --ds-color-blue-900: #{$ds-color-blue-900};
+  --ds-color-blue-950: #{$ds-color-blue-950};
 
   // Teal
-  --ds-color-teal-50: #f1f8f8;
-  --ds-color-teal-100: #e3f2f1;
-  --ds-color-teal-200: #c1e2df;
-  --ds-color-teal-300: #9ed1cd;
-  --ds-color-teal-400: #6dbab4;
-  --ds-color-teal-500: #4a9d96;
-  --ds-color-teal-600: #438e88;
-  --ds-color-teal-700: #3b7d77;
-  --ds-color-teal-800: #2f6460;
-  --ds-color-teal-900: #244c49;
-  --ds-color-teal-950: #193432;
+  --ds-color-teal-50: #{$ds-color-teal-50};
+  --ds-color-teal-100: #{$ds-color-teal-100};
+  --ds-color-teal-200: #{$ds-color-teal-200};
+  --ds-color-teal-300: #{$ds-color-teal-300};
+  --ds-color-teal-400: #{$ds-color-teal-400};
+  --ds-color-teal-500: #{$ds-color-teal-500};
+  --ds-color-teal-600: #{$ds-color-teal-600};
+  --ds-color-teal-700: #{$ds-color-teal-700};
+  --ds-color-teal-800: #{$ds-color-teal-800};
+  --ds-color-teal-900: #{$ds-color-teal-900};
+  --ds-color-teal-950: #{$ds-color-teal-950};
 
   // Black and white
-  --ds-color-white: #fff;
-  --ds-color-black: #000;
+  --ds-color-white: #{$ds-color-white};
+  --ds-color-black: #{$ds-color-black};
 }

+ 2 - 0
services/web/frontend/stylesheets/ds/components/all.scss

@@ -1,2 +1,4 @@
 @import 'button';
 @import 'form-control';
+@import 'notification';
+@import 'selection-group';

+ 194 - 2
services/web/frontend/stylesheets/ds/components/form-control.scss

@@ -1,6 +1,10 @@
 @use 'sass:math';
 @import '../mixins';
 
+:root {
+  --ds-form-check-input-size: 1em;
+}
+
 .form-control-ds,
 .form-group-ds,
 .form-text-ds,
@@ -43,9 +47,15 @@ input.form-control.form-control-ds {
   &:read-only {
     background: var(--ds-color-neutral-200);
     color: var(--ds-color-text-secondary);
+
+    &.select-trigger {
+      background: transparent;
+      color: var(--ds-color-neutral-950);
+    }
   }
 
-  &:disabled {
+  &:disabled,
+  &:disabled.select-trigger {
     color: var(--ds-color-text-disabled);
     background: var(--ds-color-neutral-100);
   }
@@ -111,6 +121,188 @@ input.form-control.form-control-ds {
   position: relative;
 }
 
-.ciam-form-input-icon {
+.form-input-icon-ds {
   font-size: var(--ds-font-size-600);
 }
+
+.form-control-wrapper-ds {
+  position: relative;
+
+  &.form-control-wrapper-disabled-ds {
+    .form-control-start-icon,
+    .form-control-end-icon {
+      & > * {
+        color: var(--ds-color-text-disabled);
+      }
+    }
+  }
+
+  .form-control-start-icon-ds,
+  .form-control-end-icon-ds {
+    position: absolute;
+    top: 0;
+    height: 100%;
+    display: flex;
+    align-items: center;
+
+    &:has(*) {
+      pointer-events: none;
+    }
+  }
+
+  .form-control-start-icon-ds {
+    left: 0;
+    padding-left: calc(var(--form-control-padding-x) + var(--bs-border-width));
+  }
+
+  .form-control-end-icon-ds {
+    right: 0;
+    padding-right: calc(var(--form-control-padding-x) + var(--bs-border-width));
+
+    .form-control-search-clear-btn {
+      @include reset-button;
+
+      display: flex;
+      justify-content: center;
+    }
+  }
+
+  --icon-width: 20px;
+  --form-control-padding-x: var(--ds-spacing-250);
+  --form-control-icon-offset-y: var(--ds-spacing-200);
+  --form-control-icon-padding: calc(
+    var(--form-control-padding-x) + var(--form-control-icon-offset-y) +
+      var(--icon-width)
+  );
+
+  .form-control-ds.form-control-offset-start-ds {
+    padding-left: var(--form-control-icon-padding);
+  }
+
+  .form-control-ds.form-control-offset-end-ds {
+    padding-right: var(--form-control-icon-padding);
+  }
+
+  &:has(:disabled) {
+    color: var(--ds-color-text-disabled);
+  }
+}
+
+.form-check-ds input.form-check-input,
+input.check-input-ds {
+  @function form-check-svg($type, $color) {
+    @if $type == 'checkbox' {
+      @return url("data:image/svg+xml,<svg width='8' height='8' viewBox='0 0 8 8' fill='none' xmlns='http://www.w3.org/2000/svg'><path d='M1 4.60011L3 7.00011L7 1.00011' fill='none' stroke='#{$color}' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/></svg>");
+    }
+
+    @if $type == 'radio' {
+      @return url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'><circle r='4' cx='4' cy='4' fill='#{$color}'/></svg>");
+    }
+  }
+
+  border-style: solid;
+  border-width: 1px;
+
+  &,
+  &:hover {
+    border-color: var(--ds-color-neutral-900);
+  }
+
+  &:checked {
+    &,
+    &:hover {
+      background-color: var(--ds-color-neutral-950);
+      border-color: var(--ds-color-neutral-950);
+    }
+  }
+
+  &:disabled {
+    background-color: var(--ds-color-neutral-200);
+    border-color: var(--ds-color-neutral-200);
+  }
+
+  &:focus-visible {
+    @include ds-focus-outline;
+  }
+
+  @mixin input-type-override($type) {
+    width: var(--ds-form-check-input-size);
+    height: var(--ds-form-check-input-size);
+
+    &:checked {
+      background-size: 8px;
+      background-image: escape-svg(form-check-svg($type, '#fff'));
+    }
+
+    &[disabled],
+    &:disabled {
+      &:checked {
+        background-image: escape-svg(
+          form-check-svg($type, $ds-color-neutral-400)
+        );
+      }
+    }
+  }
+
+  &[type='checkbox'] {
+    @include input-type-override('checkbox');
+
+    border-radius: var(--ds-border-radius-100);
+  }
+
+  &[type='radio'] {
+    @include input-type-override('radio');
+
+    border-radius: 50%;
+  }
+}
+
+input.check-input-ds {
+  appearance: none;
+  background-repeat: no-repeat;
+  background-position: center;
+  background-size: contain;
+  margin: var(--ds-spacing-100);
+
+  &[type='radio'] {
+    border-radius: 50%;
+  }
+}
+
+.form-check-ds {
+  --ds-form-check-input-margin: var(--ds-spacing-100);
+  --ds-form-check-label-gap: var(--ds-spacing-200);
+  --ds-form-check-box-size: 1em;
+
+  padding-left: calc(
+    var(--ds-form-check-box-size) + 2 * var(--ds-form-check-input-margin) +
+      var(--ds-form-check-label-gap)
+  );
+
+  &:has(.form-check-input:focus-visible) {
+    @include ds-focus-outline;
+
+    border-radius: var(--ds-border-radius-100);
+  }
+
+  .form-check-label {
+    @include ds-body-md-regular;
+  }
+
+  .form-check-input {
+    margin-left: calc(
+      -1 *
+        (
+          var(--ds-form-check-box-size) + var(--ds-form-check-input-margin) +
+            var(--ds-form-check-label-gap)
+        )
+    );
+    margin-top: var(--ds-form-check-input-margin);
+    margin-bottom: var(--ds-form-check-input-margin);
+
+    &:focus,
+    &:focus-visible {
+      box-shadow: none;
+    }
+  }
+}

+ 30 - 0
services/web/frontend/stylesheets/ds/components/notification.scss

@@ -0,0 +1,30 @@
+@use 'sass:math';
+
+.notification.notification-ds {
+  @include ds-body-sm-regular;
+
+  color: var(--ds-color-text-primary);
+  padding: 0 var(--ds-spacing-400);
+  border-width: 0;
+  border-radius: var(--ds-border-radius-200);
+  gap: var(--ds-spacing-300);
+
+  .notification-icon {
+    font-size: math.div(20em, 14);
+    display: flex;
+    align-items: center;
+    padding: var(--ds-spacing-50);
+  }
+
+  .notification-content {
+    padding: var(--ds-spacing-400) 0;
+  }
+
+  &.notification-type-error {
+    background-color: var(--ds-color-red-50);
+  }
+
+  &.notification-type-info {
+    background-color: var(--ds-color-blue-50);
+  }
+}

+ 40 - 0
services/web/frontend/stylesheets/ds/components/selection-group.scss

@@ -0,0 +1,40 @@
+.selection-group-ds {
+  display: flex;
+  flex-direction: column;
+  gap: var(--ds-spacing-400);
+}
+
+.selection-group-ds-list {
+  display: flex;
+  flex-direction: column;
+  gap: var(--ds-spacing-200);
+  list-style: none;
+  padding: 0;
+  margin: 0;
+}
+
+.selection-group-ds-item label {
+  display: flex;
+  gap: var(--ds-spacing-200);
+  padding: var(--ds-spacing-250);
+  border: 1px solid var(--ds-color-neutral-300);
+  border-radius: var(--ds-border-radius-200);
+  cursor: pointer;
+
+  &:has(input:checked) {
+    border-color: var(--ds-color-neutral-950);
+    background-color: var(--ds-color-neutral-50);
+  }
+
+  &:has(input:focus-visible, textarea:focus-visible) {
+    @include ds-focus-outline;
+
+    border-radius: var(--ds-border-radius-100);
+
+    .form-check-ds,
+    input:focus-visible,
+    textarea:focus-visible {
+      outline: none;
+    }
+  }
+}

+ 10 - 1
services/web/frontend/stylesheets/pages/onboarding-confirm-email-ciam.scss

@@ -12,10 +12,19 @@
   .confirm-email-success-form {
     display: flex;
     flex: 1 1 auto;
-    align-items: center;
     flex-direction: column;
     justify-content: center;
     text-align: center;
+    min-height: 400px;
+
+    p {
+      margin: 0;
+    }
+
+    .form-actions {
+      margin-top: var(--ds-spacing-600);
+      margin-bottom: var(--ds-spacing-400);
+    }
   }
 
   .confirm-email-form .confirm-email-form-inner {

+ 3 - 0
services/web/public/img/ciam-check-green.svg

@@ -0,0 +1,3 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path d="M14.354 4.85403L6.35403 12.854C6.30759 12.9005 6.25245 12.9374 6.19175 12.9626C6.13105 12.9877 6.06599 13.0007 6.00028 13.0007C5.93457 13.0007 5.86951 12.9877 5.80881 12.9626C5.74811 12.9374 5.69296 12.9005 5.64653 12.854L2.14653 9.35403C2.05271 9.26021 2 9.13296 2 9.00028C2 8.8676 2.05271 8.74035 2.14653 8.64653C2.24035 8.55271 2.3676 8.5 2.50028 8.5C2.63296 8.5 2.76021 8.55271 2.85403 8.64653L6.00028 11.7934L13.6465 4.14653C13.7403 4.05271 13.8676 4 14.0003 4C14.133 4 14.2602 4.05271 14.354 4.14653C14.4478 4.24035 14.5006 4.3676 14.5006 4.50028C14.5006 4.63296 14.4478 4.76021 14.354 4.85403Z" fill="#26B072"/>
+</svg>