Skip to content

Commit

Permalink
Support getting started guide on Daml Hub (digital-asset#12878)
Browse files Browse the repository at this point in the history
* Support getting started guide on Daml Hub

This is annoyingly complex so let me try to provide an explanation for
the changes:

1. On Daml hub, we need to use a separate token for the public party
   and the user party.
2. This means that we need separate contexts. I cannot get access to
   the default context (not exposed) so annoyingly even for the user context we need to
   use a custom context
3. The way to get access to the public party in Daml hub is via a
   hook that reads it from the context. However, we cannot call that
   within the login callback so the way things work now that we login
   immediately show a "Logging in..." loading screen while we run the
   background query. This is actually kinda nice since it means
   something happens immediately after clicking login.

I’m sure there are better ways of handling this, my react foo is very
weak but this is what I managed to get to work.

Tested locally as well as on Daml hub and both work fine.

changelog_begin
changelog_end

.

.

.

.

.

.

.

* s/any/void/

* fmt
  • Loading branch information
cocreature authored Feb 11, 2022
1 parent cab76a9 commit a230de2
Show file tree
Hide file tree
Showing 9 changed files with 404 additions and 262 deletions.
10 changes: 5 additions & 5 deletions templates/create-daml-app-test-resources/messaging.patch
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ diff -Naur templates/create-daml-app/ui/src/components/MessageEdit.tsx templates
+import { Form, Button } from 'semantic-ui-react';
+import { Party } from '@daml/types';
+import { User } from '@daml.js/create-daml-app';
+import { useParty, useLedger } from '@daml/react';
+import { userContext } from './App';
+
+type Props = {
+ followers: Party[];
Expand All @@ -89,11 +89,11 @@ diff -Naur templates/create-daml-app/ui/src/components/MessageEdit.tsx templates
+ * React component to edit a message to send to a follower.
+ */
+const MessageEdit: React.FC<Props> = ({followers, partyToAlias}) => {
+ const sender = useParty();
+ const sender = userContext.useParty();
+ const [receiver, setReceiver] = React.useState<string | undefined>();
+ const [content, setContent] = React.useState("");
+ const [isSubmitting, setIsSubmitting] = React.useState(false);
+ const ledger = useLedger();
+ const ledger = userContext.useLedger();
+
+ const submitMessage = async (event: React.FormEvent) => {
+ try {
Expand Down Expand Up @@ -150,7 +150,7 @@ diff -Naur templates/create-daml-app/ui/src/components/MessageList.tsx templates
+import React from 'react'
+import { List, ListItem } from 'semantic-ui-react';
+import { User } from '@daml.js/create-daml-app';
+import { useStreamQueries } from '@daml/react';
+import { userContext } from './App';
+
+type Props = {
+ partyToAlias: Map<string, string>
Expand All @@ -159,7 +159,7 @@ diff -Naur templates/create-daml-app/ui/src/components/MessageList.tsx templates
+ * React component displaying the list of messages for the current user.
+ */
+const MessageList: React.FC<Props> = ({partyToAlias}) => {
+ const messagesResult = useStreamQueries(User.Message);
+ const messagesResult = userContext.useStreamQueries(User.Message);
+
+ return (
+ <List relaxed>
Expand Down
10 changes: 9 additions & 1 deletion templates/create-daml-app/ui/src/Credentials.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,19 @@

import { User } from "@daml/ledger";

// This is used to find the public party.
// On Daml hub, we use @daml/hub-react for this.
// Locally we infer it from the token.
export type PublicParty = {
usePublicParty: () => string | undefined;
setup: () => void;
};

export type Credentials = {
party: string;
publicParty: string;
token: string;
user: User;
getPublicParty: () => PublicParty;
};

export default Credentials;
82 changes: 62 additions & 20 deletions templates/create-daml-app/ui/src/components/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,26 @@
import React from "react";
import LoginScreen from "./LoginScreen";
import MainScreen from "./MainScreen";
import DamlLedger from "@daml/react";
import { damlHubLogout } from "@daml/hub-react";
import { createLedgerContext } from "@daml/react";
import DamlHub, {
damlHubLogout,
isRunningOnHub,
usePublicParty,
usePublicToken,
} from "@daml/hub-react";
import Credentials from "../Credentials";
import { authConfig } from "../config";

// Context for the party of the user.
export const userContext = createLedgerContext();
// Context for the public party used to query user aliases.
// On Daml hub, this is a separate context. Locally, we have a single
// token that has actAs claims for the user’s party and readAs claims for
// the public party so we reuse the user context.
export const publicContext = isRunningOnHub()
? createLedgerContext()
: userContext;

/**
* React component for the entry point into the application.
*/
Expand All @@ -17,24 +32,51 @@ const App: React.FC = () => {
const [credentials, setCredentials] = React.useState<
Credentials | undefined
>();

return credentials ? (
<DamlLedger
token={credentials.token}
party={credentials.party}
user={credentials.user}>
<MainScreen
onLogout={() => {
if (authConfig.provider === "daml-hub") {
damlHubLogout();
}
setCredentials(undefined);
}}
/>
</DamlLedger>
) : (
<LoginScreen onLogin={setCredentials} />
);
if (credentials) {
const PublicPartyLedger: React.FC = ({ children }) => {
const publicToken = usePublicToken();
const publicParty = usePublicParty();
if (publicToken && publicParty) {
return (
<publicContext.DamlLedger
token={publicToken.token}
party={publicParty}>
{children}
</publicContext.DamlLedger>
);
} else {
return <h1>Loading ...</h1>;
}
};
const Wrap: React.FC = ({ children }) =>
isRunningOnHub() ? (
<DamlHub token={credentials.token}>
<PublicPartyLedger>{children}</PublicPartyLedger>
</DamlHub>
) : (
<div>{children}</div>
);
return (
<Wrap>
<userContext.DamlLedger
token={credentials.token}
party={credentials.party}
user={credentials.user}>
<MainScreen
getPublicParty={credentials.getPublicParty}
onLogout={() => {
if (authConfig.provider === "daml-hub") {
damlHubLogout();
}
setCredentials(undefined);
}}
/>
</userContext.DamlLedger>
</Wrap>
);
} else {
return <LoginScreen onLogin={setCredentials} />;
}
};
// APP_END

Expand Down
214 changes: 214 additions & 0 deletions templates/create-daml-app/ui/src/components/LoginScreen.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
// Copyright (c) 2022 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import React, { useCallback, useState } from "react";
import { Button, Form, Grid, Header, Image, Segment } from "semantic-ui-react";
import Credentials, { PublicParty } from "../Credentials";
import Ledger from "@daml/ledger";
import {
DamlHubLogin as DamlHubLoginBtn,
usePublicParty,
} from "@daml/hub-react";
import { authConfig, Insecure } from "../config";
import { useAuth0 } from "@auth0/auth0-react";

type Props = {
onLogin: (credentials: Credentials) => void;
};

/**
* React component for the login screen of the `App`.
*/
const LoginScreen: React.FC<Props> = ({ onLogin }) => {
const login = useCallback(
async (credentials: Credentials) => {
onLogin(credentials);
},
[onLogin],
);

const wrap: (c: JSX.Element) => JSX.Element = component => (
<Grid textAlign="center" style={{ height: "100vh" }} verticalAlign="middle">
<Grid.Column style={{ maxWidth: 450 }}>
<Header
as="h1"
textAlign="center"
size="huge"
style={{ color: "#223668" }}>
<Header.Content>
Create
<Image
as="a"
href="https://www.daml.com/"
target="_blank"
src="/daml.svg"
alt="Daml Logo"
spaced
size="small"
verticalAlign="bottom"
/>
App
</Header.Content>
</Header>
<Form size="large" className="test-select-login-screen">
<Segment>{component}</Segment>
</Form>
</Grid.Column>
</Grid>
);

const InsecureLogin: React.FC<{ auth: Insecure }> = ({ auth }) => {
const [username, setUsername] = React.useState("");

const handleLogin = async (event: React.FormEvent) => {
event.preventDefault();
const token = auth.makeToken(username);
const ledger = new Ledger({ token: token });
const primaryParty: string = await auth.userManagement
.primaryParty(username, ledger)
.catch(error => {
const errorMsg =
error instanceof Error ? error.toString() : JSON.stringify(error);
alert(`Failed to login as '${username}':\n${errorMsg}`);
throw error;
});

const useGetPublicParty = (): PublicParty => {
const [publicParty, setPublicParty] = useState<string | undefined>(
undefined,
);
const setup = () => {
const fn = async () => {
const publicParty = await auth.userManagement
.publicParty(username, ledger)
.catch(error => {
const errorMsg =
error instanceof Error
? error.toString()
: JSON.stringify(error);
alert(
`Failed to find primary party for user '${username}':\n${errorMsg}`,
);
throw error;
});
// todo stop yolowing error handling
setPublicParty(publicParty);
};
fn();
};
return { usePublicParty: () => publicParty, setup: setup };
};
await login({
user: { userId: username, primaryParty: primaryParty },
party: primaryParty,
token: auth.makeToken(username),
getPublicParty: useGetPublicParty,
});
};

return wrap(
<>
{/* FORM_BEGIN */}
<Form.Input
fluid
placeholder="Username"
value={username}
className="test-select-username-field"
onChange={(e, { value }) => setUsername(value?.toString() ?? "")}
/>
<Button
primary
fluid
className="test-select-login-button"
onClick={handleLogin}>
Log in
</Button>
{/* FORM_END */}
</>,
);
};

const DamlHubLogin: React.FC = () =>
wrap(
<DamlHubLoginBtn
onLogin={creds => {
if (creds) {
login({
party: creds.party,
user: { userId: creds.partyName, primaryParty: creds.party },
token: creds.token,
getPublicParty: () => ({
usePublicParty: () => usePublicParty(),
setup: () => {},
}),
});
}
}}
options={{
method: {
button: {
render: () => <Button primary fluid />,
},
},
}}
/>,
);

const Auth0Login: React.FC = () => {
const {
user,
isAuthenticated,
isLoading,
loginWithRedirect,
getAccessTokenSilently,
} = useAuth0();
(async function () {
if (isLoading === false && isAuthenticated === true) {
if (user !== undefined) {
const party = user["https://daml.com/ledger-api"];
const creds: Credentials = {
user: {
userId: user.email ?? user.name ?? party,
primaryParty: party,
},
party: party,
token: await getAccessTokenSilently({
audience: "https://daml.com/ledger-api",
}),
getPublicParty: () => {
throw Error("FIXME");
},
};
login(creds);
}
}
})();
return wrap(
<Button
primary
fluid
className="test-select-login-button"
disabled={isLoading || isAuthenticated}
loading={isLoading || isAuthenticated}
onClick={loginWithRedirect}>
Log in
</Button>,
);
};

if (authConfig.provider === "none") {
} else if (authConfig.provider === "daml-hub") {
} else if (authConfig.provider === "auth0") {
}
return authConfig.provider === "none" ? (
<InsecureLogin auth={authConfig} />
) : authConfig.provider === "daml-hub" ? (
<DamlHubLogin />
) : authConfig.provider === "auth0" ? (
<Auth0Login />
) : (
<div>Invalid configuation.</div>
);
};

export default LoginScreen;
Loading

0 comments on commit a230de2

Please sign in to comment.