React Nativeのアプリケーション開発にExpoを利用している場合にはClerkというユーザ認証/管理のクラウドサービスを利用することができます。React Nativeに限らず手軽に認証機能を実装できるということでNext.jsなどのフレームワークでも人気のサービスです。商用のサービスですがFree planが用意されているので月間10,000のアクティブユーザまで無料で利用することができます。

Clerkを使うメリットの一つにSignInやSignOutのUIが事前に準備されている点が挙げられますが、React Native用にはそれらのUIは準備されていません。SignIn, SignOut画面を作成する必要がありますがドキュメントに実装例が掲載されているのでそのコードを活用することができます。

本文書では認証機能に注目しているのでスタイルは全く設定していません。

ClerkについてはNext.jsでの設定方法の記事を公開しているのでどのような違いがあるのかも確認できます。

clerkのサインアップ

clerkはクラウドサービスなのでサービスを利用するためにアカウントを作成する必要があります。サインアップにはクレジットカードの情報等は必要なくメールアドレスやGmailアカウントを使用して利用することができます。

https://clerk.com/の右側上部の”Get started”や画面中央にある”Start building for free”のボタンをクリックしてサインアップを行います。

Clerkトップ画面
Clerkトップ画面

GitHub, Googleアカウントかメールアドレスが利用できます。

サインアップ画面
サインアップ画面

サインアップが完了するとアプリケーションを作成するためのApplications画面が表示されます。”Create application”をクリックしてください。

アプリケーション作成画面
アプリケーション作成画面

アプリケーションの作成画面ではApplicaton nameと認証に利用するオプションを選択します。React Nativeでは右側に表示されているUIは準備されていません。利用するフレームワークによっては右側に表示されるUIを利用することができます。ここではoptionsはEmailのみ選択し、Application nameな任意の名前をつけてください。optionsを選択したら”Create application”ボタンをクリックしてください。

アプリケーションの作成
アプリケーションの作成

clerkを利用するフレームワークを選択すると各フレームワークの設定項目が表示されます。まず.envファイルにアプリケーションで利用するAPI Keysを設定するように記述されています。

フレームワークの選択
フレームワークの選択

ここまででClerk側での設定は終わりです。

プロジェクトの作成

clerkでのサインアップが完了してAPI keysの値が確認できたら、React Navite用のフレームワークであるExpoを利用してプロジェクトの作成を行います。コマンドは”npx create-expo-app@latest”です。


% npx create-expo-app@latest

プロジェクト名には任意の名前をつけることができるので本文書では”react-native-auth-clerk”としています。プロジェクトディレクトリに移動してExpoでclerkを利用するためのSDKのライブラリのインストールを行います。


% cd react-native-auth-clerk
% npm install @clerk/clerk-expo

プロジェクトを作成するとappディレクトリの下にはさまざまなファイルが事前に作成されているので_layout.tsxファイルを除いてすべてを削除します。

認証設定

clerkのページに表示されていたAPI keyを設定するたプロジェクトディレクトリ直下に.envファイルを作成します。作成した.envファイルにAPI keyをコピー&ペーストします。


EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_dG9waWNhbdjIMNE9famfauY2xlcfadadmsuYWNjb3VudHMuZGV2JA
上記の設定を利用しても動作しないので各自がclerkにサインアップを行いアプリケーションを作成して取得する必要があります。

.envファイルの作成が完了したらapp/_layout.tsxファイルに以下のコードを記述します。


import { ClerkProvider, ClerkLoaded } from '@clerk/clerk-expo';
import { Slot } from 'expo-router';

const publishableKey = process.env.EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY!;

if (!publishableKey) {
  throw new Error(
    'Missing Publishable Key. Please set EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY in your .env'
  );
}

function RootLayoutNav() {
  return (
    <ClerkProvider publishableKey={publishableKey}>
      <ClerkLoaded>
        <Slot />
      </ClerkLoaded>
    </ClerkProvider>
  );
}

export default RootLayoutNav;

ClerkのSDKに含まれる関数やコンポーネントはClerkProviderでラップされたChildrenでのみ利用することができます。ClerkLoadedはClerkのAPIのLoadが完了するまでChildのコンテンツを表示させないためのものです。.envファイルで設定したAPI keyもpublicshableKeyとして利用しています。

appディレクトリの直下に(home)ディレクトリを作成して_layout.tsx, index.tsx, about.tsxファイルを作成します。_layout.tsxファイルではTab Navigationを設定します。


import { Tabs } from 'expo-router';
export default function Layout() {
  return <Tabs />;
}

NavigationにはExpo Routerが利用されています。Expo Routerについてはこちらの記事で公開しています。

index.tsx, about.tsxは下記のコードを記述します。


import { Text, View } from 'react-native';

export default function Page() {
  return (
    <View>
      <Text>Hello World</Text>
    </View>
  );
}

import { Text } from 'react-native';

export default function Page() {
  return <Text>About page</Text>;
}

npm startコマンドを実行して開発サーバを起動します。XcodeのSimulatorを利用してアプリケーションの動作確認を行います。動作確認は”Expo Go”アプリでも可能です。

Tabs Navigationを設定しているのでフッターのindex, aboutのTabボタンでページの移動を行うことができます。

Hello world
Hello world

Tab Navigationを設定した(home)以下のページに対してサインインしたユーザのみアクセスできるように設定を行います。(home)ディレクトリの_layout.tsxファイルにサインインしているかどうかの分岐処理を追加します。


import { useAuth } from '@clerk/clerk-expo';
import { Tabs } from 'expo-router';

export default function Layout() {
  const { isSignedIn } = useAuth();
  if (!isSignedIn) {
    return null;
  }
  return <Tabs />;
}

useAuth Hookでは現在のユーザのステータスを確認することができます。isSignInはBoolean値を持ち、サインインしている場合はtrue、していない場合にはfalseとなります。

サインインしていない場合はnullに設定しているので画面は真っ白になります。

画面に何も表示されない場合
画面に何も表示されない場合

SignUp, SignIn画面の作成

サインインしていない場合にはSignUpまたはSignIn画面が表示されるようにページの作成を行います。

(auth)ディレクトリを作成して_layout.tsx, sign-in.tsx, sign-up.tsxファイルを作成します。コードはドキュメントを参考に行なっています。(https://clerk.com/docs/quickstarts/expo#add-sign-up-and-sign-in-pages)

(auth)/_layout.tsxファイルではuseAuthのisSignedInプロパティの値を利用してサインインしている場合にアクセスがあった場合には”/”にリダイレクトされます。”/”にリダイレクトすると(home)/index.tsxファイルの内容が表示されます。


import { Redirect, Stack } from 'expo-router';
import { useAuth } from '@clerk/clerk-expo';

export default function AuthRoutesLayout() {
  const { isSignedIn } = useAuth();

  if (isSignedIn) {
    return <Redirect href={'/'} />;
  }

  return <Stack />;
}

sign-up.tsxファイルは以下のコードを記述します。


import * as React from 'react';
import { TextInput, Button, View } from 'react-native';
import { useSignUp } from '@clerk/clerk-expo';
import { useRouter } from 'expo-router';

export default function SignUpScreen() {
  const { isLoaded, signUp, setActive } = useSignUp();
  const router = useRouter();

  const [emailAddress, setEmailAddress] = React.useState('');
  const [password, setPassword] = React.useState('');
  const [pendingVerification, setPendingVerification] = React.useState(false);
  const [code, setCode] = React.useState('');

  const onSignUpPress = async () => {
    if (!isLoaded) {
      return;
    }

    try {
      await signUp.create({
        emailAddress,
        password,
      });

      await signUp.prepareEmailAddressVerification({ strategy: 'email_code' });

      setPendingVerification(true);
    } catch (err: any) {
      // See https://clerk.com/docs/custom-flows/error-handling
      // for more info on error handling
      console.error(JSON.stringify(err, null, 2));
    }
  };

  const onPressVerify = async () => {
    if (!isLoaded) {
      return;
    }

    try {
      const completeSignUp = await signUp.attemptEmailAddressVerification({
        code,
      });

      if (completeSignUp.status === 'complete') {
        await setActive({ session: completeSignUp.createdSessionId });
        router.replace('/');
      } else {
        console.error(JSON.stringify(completeSignUp, null, 2));
      }
    } catch (err: any) {
      // See https://clerk.com/docs/custom-flows/error-handling
      // for more info on error handling
      console.error(JSON.stringify(err, null, 2));
    }
  };

  return (
    <View>
      {!pendingVerification && (
        <>
          <TextInput
            autoCapitalize="none"
            value={emailAddress}
            placeholder="Email..."
            onChangeText={(email) => setEmailAddress(email)}
          />
          <TextInput
            value={password}
            placeholder="Password..."
            secureTextEntry={true}
            onChangeText={(password) => setPassword(password)}
          />
          <Button title="Sign Up" onPress={onSignUpPress} />
        </>
      )}
      {pendingVerification && (
        <>
          <TextInput
            value={code}
            placeholder="Code..."
            onChangeText={(code) => setCode(code)}
          />
          <Button title="Verify Email" onPress={onPressVerify} />
        </>
      )}
    </View>
  );
}

sign-in.tsxファイルは以下のコードを記述します。


import { useSignIn } from '@clerk/clerk-expo';
import { Link, useRouter } from 'expo-router';
import { Text, TextInput, Button, View } from 'react-native';
import React from 'react';

export default function Page() {
  const { signIn, setActive, isLoaded } = useSignIn();
  const router = useRouter();

  const [emailAddress, setEmailAddress] = React.useState('');
  const [password, setPassword] = React.useState('');

  const onSignInPress = React.useCallback(async () => {
    if (!isLoaded) {
      return;
    }

    try {
      const signInAttempt = await signIn.create({
        identifier: emailAddress,
        password,
      });

      if (signInAttempt.status === 'complete') {
        await setActive({ session: signInAttempt.createdSessionId });
        router.replace('/');
      } else {
        // See https://clerk.com/docs/custom-flows/error-handling
        // for more info on error handling
        console.error(JSON.stringify(signInAttempt, null, 2));
      }
    } catch (err: any) {
      console.error(JSON.stringify(err, null, 2));
    }
  }, [isLoaded, emailAddress, password]);

  return (
    <View>
      <TextInput
        autoCapitalize="none"
        value={emailAddress}
        placeholder="Email..."
        onChangeText={(emailAddress) => setEmailAddress(emailAddress)}
      />
      <TextInput
        value={password}
        placeholder="Password..."
        secureTextEntry={true}
        onChangeText={(password) => setPassword(password)}
      />
      <Button title="Sign In" onPress={onSignInPress} />
      <View>
        <Text>Don't have an account?</Text>
        <Link href="/sign-up">
          <Text>Sign up</Text>
        </Link>
      </View>
    </View>
  );
}

これらのファイルを作成後は(home)ディレクトリの_layout.tsxファイルでサインインが行われていない場合にsign-inページにリダイレクトされるように設定します。先ほどnullに設定した箇所です。


import { useAuth } from '@clerk/clerk-expo';
import { Redirect, Tabs } from 'expo-router';

export default function Layout() {
  const { isSignedIn } = useAuth();
  if (!isSignedIn) {
    return <Redirect href={'/sign-in'} />;
  }
  return <Tabs />;
}

設定後にアプリケーションにアクセスするとサインインが完了していないのでSign-In画面が表示されます。

サインイン画面

最初なのでユーザのサインアップが必要なので”Sign up”の文字列をクリックしてsign-up画面に移動します。

サインアップ画面
サインアップ画面

スタイルが設定されていないのでどこにInput要素があるのかわかりにくいですが、Email, Passwordの文字が表示されている箇所がそれぞれの入力箇所です。

EmailとPasswordを利用してサインアップを行ってください。”Sign Up”ボタンをクリックして入力した内容に問題がない場合は、入力したメールアドレス宛にメールが送信され、記述されているCodeを入力するとサインアップが完全に完了します。もしエラーがある場合は”npm start”コマンドを実行したターミナルにエラーメッセージが表示されます。XCodeのシュミレーターでも確認できます。

Codeの入力画面です。

Verify Code画面
Verify Code画面

送信されてくるメールには下記の内容が記述されているので表示されているCodeを画面で入力してください。

送信されてくるメールの内容
送信されてくるメールの内容

コードを入力すると”/”にリダイレクトされHello Worldが表示されます。

ユーザのサインアップが完了するとclerkのダッシュボード上に登録したユーザの情報が表示されます。

ユーザ登録直後のDashboard画面
ユーザ登録直後のDashboard画面

サインインしたユーザのメールアドレスをuseUser Hookから戻されるuserオブジェクトを利用して(home)/index.tsxファイルで表示することもできます。


import { useUser } from '@clerk/clerk-expo';
import { Text, View } from 'react-native';

export default function Page() {
  const { user } = useUser();
  return (
    <View>
      <Text>Hello {user?.emailAddresses[0].emailAddress}</Text>
    </View>
  );
}
ユーザのメールアドレスの表示
ユーザのメールアドレスの表示

サインアップ処理ができ、アクセス制限を行ったページへのアクセスができることを確認できました。

SignOutの設定

SignUpを行うサインインを行うことができたのでSignOutが行えるようにヘッダー部分にSignOutボタンを設定します。

(home)/_layout.jsファイルでTabs.Screenを利用してoptions propsでheaderRightを設定します。signOut関数はuseAuth Hookの戻り値に含まれています。


import { useAuth } from '@clerk/clerk-expo';
import { Redirect, Tabs } from 'expo-router';
import { Button, Text } from 'react-native';

export default function Layout() {
  const { isSignedIn, signOut } = useAuth();
  if (!isSignedIn) {
    return <Redirect href={'/sign-in'} />;
  }
  return (
    <Tabs>
      <Tabs.Screen
        name="index"
        options={{
          headerRight: () => (
            <Button onPress={() => signOut()} title="SignOut" />
          ),
        }}
      />
    </Tabs>
  );
}
SignOutボタンの表示
SignOutボタンの表示

SignOutボタンをクリックするとサインアウトが行われサインイン画面が表示されます。サインアップが完了しているのでサインアップしたメールアドレスと設定したパスワードを利用することでサインインが行われ”/”にリダイレクトされます。

サインアップとサインインの画面の作成は必要ですが、比較的簡単にReact Nativeのアプリケーションに認証機能を追加することができました。