@medplum/react 1.0.6 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,6 @@
1
+ /// <reference types="react" />
2
+ import { OperationOutcomeIssue } from '@medplum/fhirtypes';
3
+ export interface OperationOutcomeAlertProps {
4
+ issues?: OperationOutcomeIssue[];
5
+ }
6
+ export declare function OperationOutcomeAlert(props: OperationOutcomeAlertProps): JSX.Element | null;
@@ -1,10 +1,24 @@
1
1
  import { BaseLoginRequest, LoginAuthenticationResponse } from '@medplum/core';
2
2
  import React from 'react';
3
3
  export interface AuthenticationFormProps extends BaseLoginRequest {
4
- readonly generatePkce?: boolean;
5
4
  readonly onForgotPassword?: () => void;
6
5
  readonly onRegister?: () => void;
7
6
  readonly handleAuthResponse: (response: LoginAuthenticationResponse) => void;
8
7
  readonly children?: React.ReactNode;
9
8
  }
10
9
  export declare function AuthenticationForm(props: AuthenticationFormProps): JSX.Element;
10
+ export interface EmailFormProps extends BaseLoginRequest {
11
+ readonly generatePkce?: boolean;
12
+ readonly onRegister?: () => void;
13
+ readonly handleAuthResponse: (response: LoginAuthenticationResponse) => void;
14
+ readonly setEmail: (email: string) => void;
15
+ readonly children?: React.ReactNode;
16
+ }
17
+ export declare function EmailForm(props: EmailFormProps): JSX.Element;
18
+ export interface PasswordFormProps extends BaseLoginRequest {
19
+ readonly email: string;
20
+ readonly onForgotPassword?: () => void;
21
+ readonly handleAuthResponse: (response: LoginAuthenticationResponse) => void;
22
+ readonly children?: React.ReactNode;
23
+ }
24
+ export declare function PasswordForm(props: PasswordFormProps): JSX.Element;
@@ -1,6 +1,7 @@
1
1
  import { BaseLoginRequest } from '@medplum/core';
2
2
  import React from 'react';
3
3
  export interface SignInFormProps extends BaseLoginRequest {
4
+ readonly login?: string;
4
5
  readonly chooseScopes?: boolean;
5
6
  readonly onSuccess?: () => void;
6
7
  readonly onForgotPassword?: () => void;
@@ -630,6 +630,13 @@
630
630
  return undefined;
631
631
  }
632
632
 
633
+ function OperationOutcomeAlert(props) {
634
+ if (!props.issues) {
635
+ return null;
636
+ }
637
+ return (React.createElement(core$1.Alert, { icon: React.createElement(icons.IconAlertCircle, { size: 16 }), color: "red" }, props.issues.map((issue) => (React.createElement("div", { "data-testid": "text-field-error", key: issue.details?.text }, issue.details?.text)))));
638
+ }
639
+
633
640
  /**
634
641
  * Dynamically loads the recaptcha script.
635
642
  * We do not want to load the script on page load unless the user needs it.
@@ -684,12 +691,11 @@
684
691
  }
685
692
  } },
686
693
  React.createElement(core$1.Center, { sx: { flexDirection: 'column' } }, props.children),
687
- issues && (React.createElement(core$1.Alert, { icon: React.createElement(icons.IconAlertCircle, { size: 16 }), color: "red" }, issues.map((issue) => (React.createElement("div", { "data-testid": "text-field-error", key: issue.details?.text }, issue.details?.text))))),
694
+ React.createElement(OperationOutcomeAlert, { issues: issues }),
688
695
  googleClientId && (React.createElement(React.Fragment, null,
689
696
  React.createElement(core$1.Group, { position: "center", p: "xl", style: { height: 70 } },
690
697
  React.createElement(GoogleButton, { googleClientId: googleClientId, handleGoogleCredential: async (response) => {
691
698
  try {
692
- await medplum.startPkce();
693
699
  props.handleAuthResponse(await medplum.startGoogleLogin({
694
700
  googleClientId: response.clientId,
695
701
  googleCredential: response.credential,
@@ -757,47 +763,79 @@
757
763
  }
758
764
 
759
765
  function AuthenticationForm(props) {
760
- const { generatePkce, onForgotPassword, onRegister, handleAuthResponse, children, ...baseLoginRequest } = props;
766
+ const [email, setEmail] = React.useState();
767
+ if (!email) {
768
+ return React.createElement(EmailForm, { setEmail: setEmail, ...props });
769
+ }
770
+ else {
771
+ return React.createElement(PasswordForm, { email: email, ...props });
772
+ }
773
+ }
774
+ function EmailForm(props) {
775
+ const { setEmail, onRegister, handleAuthResponse, children, ...baseLoginRequest } = props;
761
776
  const medplum = useMedplum();
762
777
  const googleClientId = getGoogleClientId(props.googleClientId);
763
- const [outcome, setOutcome] = React.useState();
764
- const issues = getIssuesForExpression(outcome, undefined);
765
- async function startPkce() {
766
- if (generatePkce) {
767
- await medplum.startPkce();
778
+ const isExternalAuth = React.useCallback(async (authMethod) => {
779
+ if (!authMethod.authorizeUrl) {
780
+ return false;
768
781
  }
769
- }
770
- return (React.createElement(Form, { style: { maxWidth: 400 }, onSubmit: (formData) => {
771
- startPkce()
772
- .then(() => medplum.startLogin({
773
- ...baseLoginRequest,
774
- email: formData.email,
775
- password: formData.password,
776
- remember: formData.remember === 'on',
777
- }))
778
- .then(handleAuthResponse)
779
- .catch(setOutcome);
780
- } },
782
+ const state = JSON.stringify({
783
+ ...(await medplum.ensureCodeChallenge(baseLoginRequest)),
784
+ domain: authMethod.domain,
785
+ });
786
+ const url = new URL(authMethod.authorizeUrl);
787
+ url.searchParams.set('state', state);
788
+ window.location.assign(url.toString());
789
+ return true;
790
+ }, [medplum, baseLoginRequest]);
791
+ const handleSubmit = React.useCallback(async (formData) => {
792
+ const authMethod = await medplum.post('auth/method', { email: formData.email });
793
+ if (!(await isExternalAuth(authMethod))) {
794
+ setEmail(formData.email);
795
+ }
796
+ }, [medplum, isExternalAuth, setEmail]);
797
+ const handleGoogleCredential = React.useCallback(async (response) => {
798
+ const authResponse = await medplum.startGoogleLogin({
799
+ ...baseLoginRequest,
800
+ googleCredential: response.credential,
801
+ });
802
+ if (!(await isExternalAuth(authResponse))) {
803
+ handleAuthResponse(authResponse);
804
+ }
805
+ }, [medplum, baseLoginRequest, isExternalAuth, handleAuthResponse]);
806
+ return (React.createElement(Form, { style: { maxWidth: 400 }, onSubmit: handleSubmit },
781
807
  React.createElement(core$1.Center, { sx: { flexDirection: 'column' } }, children),
782
- issues && (React.createElement(core$1.Alert, { icon: React.createElement(icons.IconAlertCircle, { size: 16 }), color: "red" }, issues.map((issue) => (React.createElement("div", { "data-testid": "text-field-error", key: issue.details?.text }, issue.details?.text))))),
783
808
  googleClientId && (React.createElement(React.Fragment, null,
784
809
  React.createElement(core$1.Group, { position: "center", p: "xl", style: { height: 70 } },
785
- React.createElement(GoogleButton, { googleClientId: googleClientId, handleGoogleCredential: (response) => {
786
- startPkce()
787
- .then(() => medplum.startGoogleLogin({
788
- ...baseLoginRequest,
789
- googleCredential: response.credential,
790
- }))
791
- .then(props.handleAuthResponse)
792
- .catch(setOutcome);
793
- } })),
810
+ React.createElement(GoogleButton, { googleClientId: googleClientId, handleGoogleCredential: handleGoogleCredential })),
794
811
  React.createElement(core$1.Divider, { label: "or", labelPosition: "center", my: "lg" }))),
812
+ React.createElement(core$1.TextInput, { name: "email", type: "email", label: "Email", placeholder: "name@domain.com", required: true, autoFocus: true }),
813
+ React.createElement(core$1.Group, { position: "apart", mt: "xl", spacing: 0, noWrap: true },
814
+ onRegister && (React.createElement(core$1.Anchor, { component: "button", type: "button", color: "dimmed", onClick: onRegister, size: "xs" }, "Register")),
815
+ React.createElement(core$1.Button, { type: "submit" }, "Next"))));
816
+ }
817
+ function PasswordForm(props) {
818
+ const { onForgotPassword, handleAuthResponse, children, ...baseLoginRequest } = props;
819
+ const medplum = useMedplum();
820
+ const [outcome, setOutcome] = React.useState();
821
+ const issues = getIssuesForExpression(outcome, undefined);
822
+ const handleSubmit = React.useCallback((formData) => {
823
+ medplum
824
+ .startLogin({
825
+ ...baseLoginRequest,
826
+ password: formData.password,
827
+ remember: formData.remember === 'on',
828
+ })
829
+ .then(handleAuthResponse)
830
+ .catch(setOutcome);
831
+ }, [medplum, baseLoginRequest, handleAuthResponse]);
832
+ return (React.createElement(Form, { style: { maxWidth: 400 }, onSubmit: handleSubmit },
833
+ React.createElement(core$1.Center, { sx: { flexDirection: 'column' } }, children),
834
+ React.createElement(OperationOutcomeAlert, { issues: issues }),
795
835
  React.createElement(core$1.Stack, { spacing: "xl" },
796
- React.createElement(core$1.TextInput, { name: "email", type: "email", label: "Email", placeholder: "name@domain.com", required: true, autoFocus: true, error: getErrorsForInput(outcome, 'email') }),
797
836
  React.createElement(core$1.PasswordInput, { name: "password", type: "password", label: "Password", autoComplete: "off", required: true, error: getErrorsForInput(outcome, 'password') })),
798
837
  React.createElement(core$1.Group, { position: "apart", mt: "xl", spacing: 0, noWrap: true },
799
838
  onForgotPassword && (React.createElement(core$1.Anchor, { component: "button", type: "button", color: "dimmed", onClick: onForgotPassword, size: "xs" }, "Forgot password")),
800
- onRegister && (React.createElement(core$1.Anchor, { component: "button", type: "button", color: "dimmed", onClick: onRegister, size: "xs" }, "Register")),
801
839
  React.createElement(core$1.Checkbox, { id: "remember", name: "remember", label: "Remember me", size: "xs", sx: { lineHeight: 1 } }),
802
840
  React.createElement(core$1.Button, { type: "submit" }, "Sign in"))));
803
841
  }
@@ -884,7 +922,22 @@
884
922
  const [login, setLogin] = React.useState(undefined);
885
923
  const [mfaRequired, setAuthenticatorRequired] = React.useState(false);
886
924
  const [memberships, setMemberships] = React.useState(undefined);
887
- function handleAuthResponse(response) {
925
+ const handleCode = React.useCallback((code) => {
926
+ if (onCode) {
927
+ onCode(code);
928
+ }
929
+ else {
930
+ medplum
931
+ .processCode(code)
932
+ .then(() => {
933
+ if (onSuccess) {
934
+ onSuccess();
935
+ }
936
+ })
937
+ .catch(console.log);
938
+ }
939
+ }, [medplum, onCode, onSuccess]);
940
+ const handleAuthResponse = React.useCallback((response) => {
888
941
  setAuthenticatorRequired(!!response.mfaRequired);
889
942
  if (response.login) {
890
943
  setLogin(response.login);
@@ -900,28 +953,21 @@
900
953
  handleCode(response.code);
901
954
  }
902
955
  }
903
- }
904
- function handleScopeResponse(response) {
956
+ }, [chooseScopes, handleCode]);
957
+ const handleScopeResponse = React.useCallback((response) => {
905
958
  handleCode(response.code);
906
- }
907
- function handleCode(code) {
908
- if (onCode) {
909
- onCode(code);
910
- }
911
- else {
959
+ }, [handleCode]);
960
+ React.useEffect(() => {
961
+ if (props.login) {
912
962
  medplum
913
- .processCode(code)
914
- .then(() => {
915
- if (onSuccess) {
916
- onSuccess();
917
- }
918
- })
919
- .catch(console.log);
963
+ .get('auth/login/' + props.login)
964
+ .then(handleAuthResponse)
965
+ .catch(console.error);
920
966
  }
921
- }
967
+ }, [medplum, props, handleAuthResponse]);
922
968
  return (React.createElement(Document, { width: 450 }, (() => {
923
969
  if (!login) {
924
- return (React.createElement(AuthenticationForm, { generatePkce: !onCode, onForgotPassword: onForgotPassword, onRegister: onRegister, handleAuthResponse: handleAuthResponse, ...baseLoginRequest }, props.children));
970
+ return (React.createElement(AuthenticationForm, { onForgotPassword: onForgotPassword, onRegister: onRegister, handleAuthResponse: handleAuthResponse, ...baseLoginRequest }, props.children));
925
971
  }
926
972
  else if (mfaRequired) {
927
973
  return React.createElement(MfaForm, { login: login, handleAuthResponse: handleAuthResponse });