Skip to content

Commit

Permalink
daml-react: add an useUser hook (digital-asset#12622)
Browse files Browse the repository at this point in the history
We add a `useUser` hook to daml-react returning the user currently
logged in the ledger participant. create-daml-app is changed
accordingly.

CHANGELOG_BEGIN
[daml-react] A `useUser` react hook is added to the daml-react
TypeScript library. It allows for easy access to the currently logged in
user of a ledger participant node for ledgers supporting user
management.
CHANGELOG_END
  • Loading branch information
Robin Krom authored Jan 27, 2022
1 parent 579fb20 commit 0610a44
Show file tree
Hide file tree
Showing 8 changed files with 50 additions and 26 deletions.
21 changes: 16 additions & 5 deletions language-support/ts/daml-react/createLedgerContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

import React, {useContext, useEffect, useMemo, useState } from 'react';
import { ContractId,Party, Template } from '@daml/types';
import Ledger, { CreateEvent, Query, Stream, StreamCloseEvent, QueryResult } from '@daml/ledger';
import Ledger, { CreateEvent, Query, Stream, StreamCloseEvent, QueryResult, User } from '@daml/ledger';

export { QueryResult } from '@daml/ledger';

Expand All @@ -13,6 +13,7 @@ export { QueryResult } from '@daml/ledger';
type DamlLedgerState = {
reloadToken: unknown;
triggerReload: () => void;
user?: User;
party: Party;
ledger: Ledger;
}
Expand All @@ -24,6 +25,7 @@ export type LedgerProps = {
token: string;
httpBaseUrl?: string;
wsBaseUrl?: string;
user?: User;
party: Party;
reconnectThreshold?: number;
}
Expand Down Expand Up @@ -63,6 +65,7 @@ export type FetchByKeysResult<T extends object, K, I extends string> = {
export type LedgerContext = {
DamlLedger: React.FC<LedgerProps>;
useParty: () => Party;
useUser: () => User;
useLedger: () => Ledger;
useQuery: <T extends object, K, I extends string>(template: Template<T, K, I>, queryFactory?: () => Query<T>, queryDeps?: readonly unknown[]) => QueryResult<T, K, I>;
useFetch: <T extends object, K, I extends string>(template: Template<T, K, I>, contractId: ContractId<T>) => FetchResult<T, K, I>;
Expand Down Expand Up @@ -91,13 +94,13 @@ export function createLedgerContext(contextName="DamlLedgerContext"): LedgerCont
// not make a new network request although they are required to refresh data.

const ledgerContext = React.createContext<DamlLedgerState | undefined>(undefined);
const DamlLedger: React.FC<LedgerProps> = ({token, httpBaseUrl, wsBaseUrl, reconnectThreshold, party, children}) => {
const DamlLedger: React.FC<LedgerProps> = ({token, httpBaseUrl, wsBaseUrl, reconnectThreshold, user, party, children}) => {
const [reloadToken, setReloadToken] = useState(0);
const ledger = useMemo(() => new Ledger({token, httpBaseUrl, wsBaseUrl, reconnectThreshold}), [token, httpBaseUrl, wsBaseUrl, reconnectThreshold]);
const state: DamlLedgerState = useMemo(() => ({
reloadToken,
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands
triggerReload: (): void => setReloadToken(x => x + 1),
triggerReload: (): void => setReloadToken((x:number) => x + 1),
user,
party,
ledger,
}), [party, ledger, reloadToken]);
Expand All @@ -121,6 +124,14 @@ export function createLedgerContext(contextName="DamlLedgerContext"): LedgerCont
return useDamlState().ledger;
}

const useUser = (): User => {
const user = useDamlState().user;
if (!user) {
throw Error(`Trying to use 'useUser' for a DamlLedger with a missing 'user' field.`);
} else
return user
}

function useQuery<T extends object, K, I extends string>(template: Template<T, K, I>, queryFactory?: () => Query<T>, queryDeps?: readonly unknown[]): QueryResult<T, K, I> {
const state = useDamlState();
const [result, setResult] = useState<QueryResult<T, K, I>>({contracts: [], loading: true});
Expand Down Expand Up @@ -285,5 +296,5 @@ export function createLedgerContext(contextName="DamlLedgerContext"): LedgerCont
return (): void => state.triggerReload();
}

return { DamlLedger, useParty, useLedger, useQuery, useFetch, useFetchByKey, useStreamQuery, useStreamQueries, useStreamFetchByKey, useStreamFetchByKeys, useReload };
return { DamlLedger, useParty, useUser, useLedger, useQuery, useFetch, useFetchByKey, useStreamQuery, useStreamQueries, useStreamFetchByKey, useStreamFetchByKeys, useReload };
}
7 changes: 6 additions & 1 deletion language-support/ts/daml-react/defaultLedgerContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

import { createLedgerContext, FetchResult, QueryResult, LedgerProps, FetchByKeysResult } from "./createLedgerContext";
import { ContractId, Party, Template } from '@daml/types';
import Ledger, { Query, StreamCloseEvent } from '@daml/ledger';
import Ledger, { Query, StreamCloseEvent, User } from '@daml/ledger';

/**
* @internal
Expand All @@ -24,6 +24,11 @@ export function DamlLedger(props: React.PropsWithChildren<LedgerProps>): React.R
*/
export function useParty(): Party { return ledgerContext.useParty(); }

/**
* React hook to get the user currently connected to the ledger participant.
*/
export function useUser(): User { return ledgerContext.useUser(); }

/**
* React Hook that returns the Ledger instance to interact with the connected Daml ledger.
*/
Expand Down
16 changes: 12 additions & 4 deletions language-support/ts/daml-react/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
/* eslint-disable @typescript-eslint/no-floating-promises */
import React, { ComponentType, useState } from 'react';
import { renderHook, RenderHookResult, act } from '@testing-library/react-hooks';
import DamlLedger, { useParty, useQuery, useFetch, useFetchByKey, useStreamQuery, useStreamQueries, useStreamFetchByKey, useStreamFetchByKeys, useReload, createLedgerContext } from './index';
import DamlLedger, { useParty, useUser, useQuery, useFetch, useFetchByKey, useStreamQuery, useStreamQueries, useStreamFetchByKey, useStreamFetchByKeys, useReload, createLedgerContext } from './index';
import { ContractId, Template } from '@daml/types';
import { Stream, StreamCloseEvent, Query } from '@daml/ledger';
import {EventEmitter} from 'events';
Expand Down Expand Up @@ -67,9 +67,10 @@ const mockStream = <T>(): [Stream <object, string, string, T>, EventEmitter] =>

const TOKEN = 'test_token';
const PARTY = 'test_party';
const USER = {userId: 'test_user'};

function renderDamlHook<P, R>(callback: (props: P) => R): RenderHookResult<P, R> {
const wrapper: ComponentType = ({children}) => React.createElement(DamlLedger, {token: TOKEN, party: PARTY, reconnectThreshold: 1337}, children);
const wrapper: ComponentType = ({children}) => React.createElement(DamlLedger, {token: TOKEN, party: PARTY, user: USER, reconnectThreshold: 1337}, children);
return renderHook(callback, {wrapper});
}

Expand All @@ -94,6 +95,11 @@ test('useParty', () => {
expect(result.current).toBe(PARTY);
});

test('useUser', () => {
const {result} = renderDamlHook(() => useUser());
expect(result.current).toBe(USER);
});

describe('useQuery', () => {
test('one shot without query', async () => {
const resolvent = ['foo'];
Expand Down Expand Up @@ -540,16 +546,18 @@ function streamk<Q, Ignored, R extends {loading: boolean}>(useFn: (template: Tem
const innerLedger = createLedgerContext();
const innerTOKEN = "inner_TOKEN";
const innerPARTY = "inner_PARTY";
const innerUSER = {userId: "inner_USER"};
const outerLedger = createLedgerContext('Outer');
const outerTOKEN = "outer_TOKEN";
const outerPARTY = "outer_PARTY";
const outerUSER = { userId: "outer_USER"};

const innerWrapper: ComponentType = ({children}) => React.createElement(innerLedger.DamlLedger, {token:innerTOKEN, party:innerPARTY}, children);
const innerWrapper: ComponentType = ({children}) => React.createElement(innerLedger.DamlLedger, {token:innerTOKEN, party:innerPARTY, user:innerUSER}, children);
const r1 = renderHook(() => innerLedger.useParty(), {wrapper:innerWrapper});

expect( r1.result.current).toBe(innerPARTY);

const outerWrapper: ComponentType = ({children}) => React.createElement(outerLedger.DamlLedger, {token:outerTOKEN, party:outerPARTY}, innerWrapper({children}) );
const outerWrapper: ComponentType = ({children}) => React.createElement(outerLedger.DamlLedger, {token:outerTOKEN, party:outerPARTY, user:outerUSER}, innerWrapper({children}) );
const r2 = renderHook(() => outerLedger.useParty(), {wrapper:outerWrapper});
expect( r2.result.current).toBe(outerPARTY);

Expand Down
4 changes: 2 additions & 2 deletions language-support/ts/daml-react/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@

export { createLedgerContext, FetchResult, LedgerContext, QueryResult, FetchByKeysResult } from './createLedgerContext';

import { DamlLedger, useParty, useLedger, useQuery, useFetch, useFetchByKey, useStreamQuery, useStreamQueries, useStreamFetchByKey, useStreamFetchByKeys, useReload } from "./defaultLedgerContext";
export { useParty, useLedger, useQuery, useFetch, useFetchByKey, useStreamQuery, useStreamQueries, useStreamFetchByKey, useStreamFetchByKeys, useReload };
import { DamlLedger, useParty, useUser, useLedger, useQuery, useFetch, useFetchByKey, useStreamQuery, useStreamQueries, useStreamFetchByKey, useStreamFetchByKeys, useReload } from "./defaultLedgerContext";
export { useParty, useUser, useLedger, useQuery, useFetch, useFetchByKey, useStreamQuery, useStreamQueries, useStreamFetchByKey, useStreamFetchByKeys, useReload };
export default DamlLedger;
4 changes: 4 additions & 0 deletions templates/create-daml-app/ui/src/Credentials.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
// Copyright (c) 2022 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

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


export type Credentials = {
party: string;
token: string;
user: User;
}

export default Credentials;
1 change: 1 addition & 0 deletions templates/create-daml-app/ui/src/components/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const App: React.FC = () => {
? <DamlLedger
token={credentials.token}
party={credentials.party}
user={credentials.user}
>
<MainScreen onLogout={() => {
if (authConfig.provider === 'daml-hub') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,8 @@ const LoginScreen: React.FC<Props> = ({onLogin}) => {
alert(`Failed to login as '${username}':\n${errorMsg}`);
throw error;
});
await login({party: primaryParty,
await login({user: {userId: username, primaryParty: primaryParty},
party: primaryParty,
token: auth.makeToken(username)});
}

Expand All @@ -99,7 +100,7 @@ const LoginScreen: React.FC<Props> = ({onLogin}) => {
<DamlHubLoginBtn
onLogin={creds => {
if (creds) {
login(creds);
login({party:creds.party, user: {userId: creds.partyName, primaryParty: creds.party}, token:creds.token});
}
}}
options={{
Expand All @@ -118,8 +119,10 @@ const LoginScreen: React.FC<Props> = ({onLogin}) => {
(async function () {
if (isLoading === false && isAuthenticated === true) {
if (user !== undefined) {
const party = user["https://daml.com/ledger-api"];
const creds: Credentials = {
party: user["https://daml.com/ledger-api"],
user: {userId: user.email ?? user.name ?? party, primaryParty: party},
party: party,
token: (await getAccessTokenSilently({
audience: "https://daml.com/ledger-api"}))};
login(creds);
Expand Down
14 changes: 3 additions & 11 deletions templates/create-daml-app/ui/src/components/MainScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@
import React from 'react'
import { Image, Menu } from 'semantic-ui-react'
import MainView from './MainView';
import {useLedger} from '@daml/react';
import {useState, useEffect} from 'react'
import {useUser} from '@daml/react';

type Props = {
onLogout: () => void;
Expand All @@ -15,14 +14,7 @@ type Props = {
* React component for the main screen of the `App`.
*/
const MainScreen: React.FC<Props> = ({onLogout}) => {
const ledger = useLedger();
const [user, setUser] = useState('');
useEffect( () =>{
(async () => {
const u = await ledger.getUser()
setUser(u.userId);
} ) ()}
, [ledger]);
const user = useUser();

return (
<>
Expand All @@ -39,7 +31,7 @@ const MainScreen: React.FC<Props> = ({onLogout}) => {
</Menu.Item>
<Menu.Menu position='right' className='test-select-main-menu'>
<Menu.Item position='right'>
You are logged in as {user}.
You are logged in as {user.userId}.
</Menu.Item>
<Menu.Item
position='right'
Expand Down

0 comments on commit 0610a44

Please sign in to comment.