Skip to content

Commit

Permalink
Support biometric in Capacitor SDK #257
Browse files Browse the repository at this point in the history
ref #256
  • Loading branch information
tung2744 authored Jan 18, 2024
2 parents 280cd63 + ec561c5 commit f0e05b7
Show file tree
Hide file tree
Showing 33 changed files with 2,351 additions and 108 deletions.
35 changes: 35 additions & 0 deletions example/capacitor/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Ionic Capacitor Example App

Example app of ionic app using capacitor.

## Project config

We need the following redirect uris to work:

```
http://localhost:8100/reauth-redirect
http://localhost:8100/oauth-redirect
com.authgear.exampleapp.capacitor://host/path
https://localhost
capacitor://localhost
```

## Run the app

Before running the app, build the latest sdk at project root.

```sh
npm i
npm run build
```

Then, in this directory, run the exmaple app.

```sh
cd example/capacitor
npm i
# Run it in ios device
npm run run-ios
# OR, run it in android device
npm run run-android
```
2 changes: 2 additions & 0 deletions example/capacitor/ios/App/App/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -47,5 +47,7 @@
<true/>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>NSFaceIDUsageDescription</key>
<string>Use Face ID to authenticate</string>
</dict>
</plist>
10 changes: 5 additions & 5 deletions example/capacitor/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion example/capacitor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
"build": "tsc && vite build",
"preview": "vite preview",
"test.unit": "vitest",
"lint": "eslint"
"lint": "eslint",
"run-ios": "cap run ios",
"run-android": "cap run android"
},
"dependencies": {
"@authgear/capacitor": "../../packages/authgear-capacitor",
Expand Down
193 changes: 189 additions & 4 deletions example/capacitor/src/pages/Home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ import authgearCapacitor, {
CancelError as CapacitorCancelError,
ColorScheme,
Page as CapacitorPage,
BiometricOptions,
BiometricAccessConstraintIOS,
BiometricLAPolicy,
BiometricAccessConstraintAndroid,
} from "@authgear/capacitor";
import {
readClientID,
Expand All @@ -59,6 +63,22 @@ const REDIRECT_URI_WEB_AUTHENTICATE = "http://localhost:8100/oauth-redirect";
const REDIRECT_URI_WEB_REAUTH = "http://localhost:8100/reauth-redirect";
const REDIRECT_URI_CAPACITOR = "com.authgear.exampleapp.capacitor://host/path";

const biometricOptions: BiometricOptions = {
ios: {
localizedReason: "Use biometric to authenticate",
constraint: BiometricAccessConstraintIOS.BiometryCurrentSet,
policy: BiometricLAPolicy.deviceOwnerAuthenticationWithBiometrics,
},
android: {
title: "Biometric Authentication",
subtitle: "Biometric authentication",
description: "Use biometric to authenticate",
negativeButtonText: "Cancel",
constraint: [BiometricAccessConstraintAndroid.BiometricStrong],
invalidatedByBiometricEnrollment: true,
},
};

function isPlatformWeb(): boolean {
return Capacitor.getPlatform() === "web";
}
Expand All @@ -83,6 +103,7 @@ function AuthgearDemo() {
const [isSSOEnabled, setIsSSOEnabled] = useState(() => {
return readIsSSOEnabled();
});
const [biometricEnabled, setBiometricEnabled] = useState<boolean>(false);

const [sessionState, setSessionState] = useState<SessionState | null>(() => {
if (isPlatformWeb()) {
Expand All @@ -105,6 +126,20 @@ function AuthgearDemo() {
return d;
}, [setSessionState]);

const updateBiometricState = useCallback(async () => {
if (isPlatformWeb()) {
return;
}

try {
await authgearCapacitor.checkBiometricSupported(biometricOptions);
const enabled = await authgearCapacitor.isBiometricEnabled();
setBiometricEnabled(enabled);
} catch (e) {
console.error(e);
}
}, []);

const showError = useCallback((e: any) => {
const json = JSON.parse(JSON.stringify(e));
json["constructor.name"] = e?.constructor?.name;
Expand All @@ -129,6 +164,7 @@ function AuthgearDemo() {
}, []);

const postConfigure = useCallback(async () => {
await updateBiometricState();
const sessionState = isPlatformWeb()
? authgearWeb.sessionState
: authgearCapacitor.sessionState;
Expand All @@ -144,7 +180,7 @@ function AuthgearDemo() {
}

setInitialized(true);
}, []);
}, [updateBiometricState]);

const configure = useCallback(async () => {
setLoading(true);
Expand Down Expand Up @@ -207,8 +243,48 @@ function AuthgearDemo() {
showError(e);
} finally {
setLoading(false);
await updateBiometricState();
}
}, [colorScheme, page, showError, showUserInfo, updateBiometricState]);

const enableBiometric = useCallback(async () => {
setLoading(true);
try {
await authgearCapacitor.enableBiometric(biometricOptions);
} catch (e: unknown) {
showError(e);
} finally {
setLoading(false);
await updateBiometricState();
}
}, [showError, updateBiometricState]);

const authenticateBiometric = useCallback(async () => {
setLoading(true);
try {
const { userInfo } = await authgearCapacitor.authenticateBiometric(
biometricOptions
);
showUserInfo(userInfo);
} catch (e: unknown) {
showError(e);
} finally {
setLoading(false);
await updateBiometricState();
}
}, [showError, showUserInfo, updateBiometricState]);

const disableBiometric = useCallback(async () => {
setLoading(true);
try {
await authgearCapacitor.disableBiometric();
} catch (e: unknown) {
showError(e);
} finally {
setLoading(false);
await updateBiometricState();
}
}, [colorScheme, page, showError, showUserInfo]);
}, [showError, updateBiometricState]);

const showAuthTime = useCallback(() => {
if (isPlatformWeb()) {
Expand All @@ -224,7 +300,7 @@ function AuthgearDemo() {
}
}, []);

const reauthenticate = useCallback(async () => {
const reauthenticateWebOnly = useCallback(async () => {
setLoading(true);
try {
if (isPlatformWeb()) {
Expand Down Expand Up @@ -260,6 +336,45 @@ function AuthgearDemo() {
}
}, [showError, colorScheme, showAuthTime]);

const reauthenticate = useCallback(async () => {
setLoading(true);
try {
if (isPlatformWeb()) {
await authgearWeb.refreshIDToken();
if (!authgearWeb.canReauthenticate()) {
throw new Error(
"canReauthenticate() returns false for the current user"
);
}

authgearWeb.startReauthentication({
redirectURI: REDIRECT_URI_WEB_REAUTH,
});
} else {
await authgearCapacitor.refreshIDToken();
if (!authgearCapacitor.canReauthenticate()) {
throw new Error(
"canReauthenticate() returns false for the current user"
);
}

await authgearCapacitor.reauthenticate(
{
redirectURI: REDIRECT_URI_CAPACITOR,
colorScheme:
colorScheme === "" ? undefined : (colorScheme as ColorScheme),
},
biometricOptions
);
showAuthTime();
}
} catch (e) {
showError(e);
} finally {
setLoading(false);
}
}, [showError, colorScheme, showAuthTime]);

const openSettings = useCallback(async () => {
if (isPlatformWeb()) {
authgearWeb.open(WebPage.Settings);
Expand Down Expand Up @@ -409,6 +524,46 @@ function AuthgearDemo() {
[reauthenticate]
);

const onClickReauthenticateWebOnly = useCallback(
(e: MouseEvent<HTMLIonButtonElement>) => {
e.preventDefault();
e.stopPropagation();

reauthenticateWebOnly();
},
[reauthenticateWebOnly]
);

const onClickEnableBiometric = useCallback(
(e: MouseEvent<HTMLIonButtonElement>) => {
e.preventDefault();
e.stopPropagation();

enableBiometric();
},
[enableBiometric]
);

const onClickAuthenticateBiometric = useCallback(
(e: MouseEvent<HTMLIonButtonElement>) => {
e.preventDefault();
e.stopPropagation();

authenticateBiometric();
},
[authenticateBiometric]
);

const onClickDisableBiometric = useCallback(
(e: MouseEvent<HTMLIonButtonElement>) => {
e.preventDefault();
e.stopPropagation();

disableBiometric();
},
[disableBiometric]
);

const onClickOpenSettings = useCallback(
(e: MouseEvent<HTMLIonButtonElement>) => {
e.preventDefault();
Expand Down Expand Up @@ -525,10 +680,40 @@ function AuthgearDemo() {
<IonButton
className="button"
disabled={!initialized || loading || !loggedIn}
onClick={onClickReauthenticate}
onClick={onClickReauthenticateWebOnly}
>
Re-authenticate
</IonButton>
<IonButton
className="button"
disabled={!initialized || loading || !loggedIn}
onClick={onClickReauthenticate}
>
Re-authenticate (biometric or web)
</IonButton>
{isPlatformWeb() ? null : (
<IonButton
className="button"
disabled={!initialized || loading || !loggedIn || biometricEnabled}
onClick={onClickEnableBiometric}
>
Enable biometric
</IonButton>
)}
<IonButton
className="button"
disabled={!initialized || loading || !biometricEnabled}
onClick={onClickDisableBiometric}
>
Disable biometric
</IonButton>
<IonButton
className="button"
disabled={!initialized || loading || loggedIn || !biometricEnabled}
onClick={onClickAuthenticateBiometric}
>
Authenticate with biometric
</IonButton>
<IonButton
className="button"
disabled={!initialized || !loggedIn}
Expand Down
Loading

0 comments on commit f0e05b7

Please sign in to comment.