Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat:enable iOS and Android examples #3

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 77 additions & 7 deletions App.js
Original file line number Diff line number Diff line change
@@ -1,24 +1,35 @@
import { getDisplayString, MedplumClient } from "@medplum/core";
import base64 from 'base-64';
import { StatusBar } from "expo-status-bar";
import { useState } from "react";
import { Button, StyleSheet, Text, View } from "react-native";
import { TextInput } from "react-native-web";
import { Button, StyleSheet, Text, TextInput, View } from "react-native";

const clientId = 'MY_CLIENT_ID';
const clientSecret = 'MY_CLIENT_SECRET';


const medplum = new MedplumClient({
// Enter your Medplum connection details here
// See MedplumClient docs for more details
// baseUrl: "http://localhost:8103/",
// clientId: 'MY_CLIENT_ID',
clientId: 'MY_CLIENT_ID',
// clientSecret: `MY_CLIENT_SECRET',
// projectId: 'MY_PROJECT_ID',
});

export default function App() {
const authHeader = base64.encode(`${clientId}:${clientSecret}`);
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");

//ideally importing Profile resource from FhirTypes and using Partial<Profile> as the type
const [profile, setProfile] = useState(undefined);

function startLogin() {
medplum.startLogin({ email, password }).then(handleAuthResponse);
// medplum.startLogin({ email, password }).then(handleAuthResponse);
medplum
.post('auth/login', { email, password, clientId })
.then(handleAuthResponse);
}

function handleAuthResponse(response) {
Expand All @@ -38,12 +49,70 @@ export default function App() {
}
}

function handleCode(code) {
medplum.processCode(code).then(setProfile);
async function handleAuthResponse(response) {
if (response.code) {
handleCode(response.code);
}
if (response.memberships) {
// TODO: Handle multiple memberships
// In a real app, you would present a list of memberships to the user
// For this example, just use the first membership
medplum
.post("auth/profile", {
login: response.login,
profile: response.memberships[0].id,
})
.then(handleAuthResponse);
}
}

async function handleCode(code) {
// medplum.processCode(code).then(setProfile);
const details = {
grant_type: 'authorization_code',
client_id: clientId,
code: code,
};
const formBody = Object.entries(details)
.map(
([key, value]) =>
encodeURIComponent(key) +
'=' +
encodeURIComponent(value),
)
.join('&');

const tokenResponse = await fetch(
'https://api.medplum.com/oauth2/token',
{
method: 'POST',
body: formBody,
headers: {
'Content-Type':
'application/x-www-form-urlencoded; charset=UTF-8',
Authorization: 'Basic ' + authHeader,
},
},
);

const { access_token } = await tokenResponse.json();
await medplum.setAccessToken(access_token);

medplum
.get('auth/me', {
headers: {
Authorization: 'Basic ' + authHeader,
},
})
.then((res) => {
setProfile(res.profile);
});
}

function signOut() {
setProfile(undefined);
setEmail('');
setPassword('');
medplum.signOut();
}

Expand All @@ -57,6 +126,7 @@ export default function App() {
style={styles.input}
placeholder="Email"
placeholderTextColor="#003f5c"
defaultValue={email}
onChangeText={(email) => setEmail(email)}
/>
</View>
Expand All @@ -65,6 +135,7 @@ export default function App() {
style={styles.input}
placeholder="Password"
placeholderTextColor="#003f5c"
defaultValue={password}
secureTextEntry={true}
onChangeText={(password) => setPassword(password)}
/>
Expand Down Expand Up @@ -92,7 +163,6 @@ const styles = StyleSheet.create({
},
input: {
height: 50,
flex: 1,
padding: 10,
marginLeft: 20,
},
Expand Down
100 changes: 100 additions & 0 deletions App.web.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { getDisplayString, MedplumClient } from "@medplum/core";
import { StatusBar } from "expo-status-bar";
import { useState } from "react";
import { Button, StyleSheet, Text, View } from "react-native";
import { TextInput } from "react-native-web";

const medplum = new MedplumClient({
// Enter your Medplum connection details here
// See MedplumClient docs for more details
// baseUrl: "http://localhost:8103/",
clientId: 'MY_CLIENT_ID',
// clientSecret: 'MY_CLIENT_SECRET',
// projectId: 'MY_PROJECT_ID',
});

export default function App() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [profile, setProfile] = useState(undefined);

function startLogin() {
medplum.startLogin({ email, password }).then(handleAuthResponse);
}

function handleAuthResponse(response) {
if (response.code) {
handleCode(response.code);
}
if (response.memberships) {
// TODO: Handle multiple memberships
// In a real app, you would present a list of memberships to the user
// For this example, just use the first membership
medplum
.post("auth/profile", {
login: response.login,
profile: response.memberships[0].id,
})
.then(handleAuthResponse);
}
}

function handleCode(code) {
medplum.processCode(code).then(setProfile);
}

function signOut() {
setProfile(undefined);
medplum.signOut();
}

return (
<View style={styles.container}>
<Text>Medplum React Native Web Example</Text>
{!profile ? (
<>
<View>
<TextInput
style={styles.input}
placeholder="Email"
placeholderTextColor="#003f5c"
onChangeText={(email) => setEmail(email)}
/>
</View>
<View>
<TextInput
style={styles.input}
placeholder="Password"
placeholderTextColor="#003f5c"
secureTextEntry={true}
onChangeText={(password) => setPassword(password)}
/>
</View>
<Button onPress={startLogin} title="Sign in" />
</>
) : (
<>
<Text>Logged in as {getDisplayString(profile)}</Text>
<Button onPress={signOut} title="Sign out" />
</>
)}
<StatusBar style="auto" />
</View>
);
}

const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#fff",
alignItems: "center",
justifyContent: "center",
gap: 10,
},
input: {
height: 50,
flex: 1,
padding: 10,
marginLeft: 20,
},
});
17 changes: 13 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

This is a basic starter app that demonstrates how to sign into Medplum with React Native.

This only demonstrates React Native in "web" mode. Android and iOS are out of scope.
This demonstrates React Native in "web" mode with the platform file of `App.web.js` as well as the React Native mobile capability using the Medplum SDK `post` and `get` methods to accomplish authentication and retrieving profile.

## Setup

Expand Down Expand Up @@ -67,17 +67,25 @@ return (

### Sign in button

Clicking on the "Sign in" button will executes the `startLogin` function:
Clicking on the "Sign in" button will executes the `startLogin` function when in web mode, or the equivalent `auth/login` for android and iOS:

```js
function startLogin() {
medplum.startLogin({ email, password }).then(handleAuthResponse);
}
```

```js
function startLogin() {
medplum
.post("auth/login", { email, password, clientId })
.then(handleAuthResponse);
}
```

### Sign in response

There are two successful response types from `startLogin`:
There are two successful response types from `startLogin` or `auth/login`:

1. If the user only has one matching profile, the response includes `code` for OAuth token exchange.
2. If the user has multiple profiles, the response includes `memberships` with project membership and profile details.
Expand Down Expand Up @@ -105,14 +113,15 @@ function handleAuthResponse(response) {

### Token exchange

Now that we have a `code`, we can follow OAuth token exchange. Call `processCode` to exchange the `code` for an access token:
Now that we have a `code`, we can follow OAuth token exchange. Call `processCode` to exchange the `code` for an access token on web or make a post request to `oauth2/token`:

```js
function handleCode(code) {
medplum.processCode(code).then(setProfile);
}
```


`processCode` sets the access token in `MedplumClient` and returns the user's profile resource.

### Display the profile
Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@
"dependencies": {
"@expo/webpack-config": "^18.0.1",
"@medplum/core": "^2.0.7",
"expo": "~48.0.7",
"expo-status-bar": "~1.4.4",
"base-64": "^1.0.0",
"expo": "^48.0.20",
"expo-status-bar": "^1.6.0",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-native": "0.71.4",
Expand Down