LoginSignup
42
35

More than 3 years have passed since last update.

【Cognito】Amazon Cognito Identity SDK for JavaScriptで動くサンプルを作ってみた #2/2【JavaScript】

Last updated at Posted at 2018-12-13

はじめに

下記記事の続きです。

【Cognito】Amazon Cognito Identity SDK for JavaScriptで動くサンプルを作ってみた #1/2【JavaScript】
【Cognito】Amazon Cognito Identity SDK for JavaScriptで動くサンプルを作ってみた #2/2【JavaScript】 ←いまココ

JavaScriptの実装を、各機能ごとに解説します。

対象のソースコードはこちらです。
https://github.com/tmiki/cognito-sample-js-webapp

アプリケーション構成

まず、ディレクトリ構成は以下の通りです。至ってよくある、JavaScriptアプリケーションです。
モジュールバンドラ・ビルドツール・開発用のWebサーバとしてWebpackを利用しています。これも昨今よくある構成です。

-+- package.json
 +- webpack.config.js
 +- /dist/
    +- index.html
    +- main.css
 +- /src/
    +- index.js 
 +- /node_modules/...
 |
 |

packages.json

中身は見ればわかるので、詳細は省略します。dependenciesとして以下の3つのモジュールが重要になります。

  • amazon-cognito-identity-js
  • aws-sdk
  • amazon-cognito-js

webpack.config.js

moduleの指定は見ればわかるので省略します。Babelを利用しています。

bindするIPアドレスが、デフォルトだと127.0.0.1なのでこれを0.0.0.0に変えています。Windows/macOSのネイティブ環境で動作させる場合はデフォルトのままで問題になりませんが、VirtualBox等でVM内で動作させている場合はWebブラウザからアクセスできなくなります。
portはとりあえず他のアプリと被らなそうなものにしています。TCP/3000やTCP/8000などは他のアプリで使っていると思いますので。

// ** omit **

const webpackConfig = {
// ** omit ** 
  devServer: {
    contentBase: DIR_DIST,
    host: '0.0.0.0',
    port: 8123
  }
};

// ** omit **

dist/index.html

対象のファイルはこちらです。
https://github.com/tmiki/cognito-sample-js-webapp/blob/master/dist/index.html

Material Design Liteを使っています。
Material Design Lite

とりあえず、各機能用のボタンを下記のように定義しています。idは「[関数名のケバブケース表記]-button」という命名規則にしています。

<!-- 途中省略 -->
        <div class="mdl-cell mdl-cell--12-col">
          <button id="sign-up-button" class="mdl-button mdl-js-button mdl-button--raised md-js-ripple-effect mdl-button--accent">Sign Up</button>
          <button id="verify-code-button" class="mdl-button mdl-js-button mdl-button--raised md-js-ripple-effect mdl-button--accent">Verify Your Code</button>
          <button id="resend-confirmation-by-email-button" class="mdl-button mdl-js-button mdl-button--raised md-js-ripple-effect mdl-button--accent">Resend confirmation by Email</button>
        </div>
        <div class="mdl-cell mdl-cell--12-col">
          <button id="reset-password-button" class="mdl-button mdl-js-button mdl-button--raised md-js-ripple-effect mdl-button--accent">Reset Password (verification code will be sent)</button>
          <button id="confirm-password-button" class="mdl-button mdl-js-button mdl-button--raised md-js-ripple-effect mdl-button--accent">Confirm Password (verification code and new password are needed)</button>
        </div>
        <div class="mdl-cell mdl-cell--12-col">
          <button id="login-button" class="mdl-button mdl-js-button mdl-button--raised md-js-ripple-effect mdl-button--accent">Login</button>
          <button id="get-aws-credentials-button" class="mdl-button mdl-js-button mdl-button--raised md-js-ripple-effect mdl-button--accent">Get AWS Credentials</button>
        </div>
         <div class="mdl-cell mdl-cell--12-col">
          <button id="logout-button" class="mdl-button mdl-js-button mdl-button--raised md-js-ripple-effect mdl-button--accent">Logout</button>
        </div>
<!-- 途中省略 -->

src/index.jsの概要

大きく以下のセクションに大別できます。
jQuery等、他のライブラリは極力利用せず、プリミティブな機能だけで実装しています。

  1. ライブラリ読み込み
  2. グローバル変数定義
  3. ユーティリティ関数定義
  4. Amazon Cognito Identity SDK for JavaScriptの各機能を実現する関数定義(※これがメイン)
  5. 上記項番4の関数をHTMLの各ボタンにaddEventListenerする処理
  6. リロード直後の初期化処理(ログインセッションがあれば読み出す処理など)

1. ライブラリ読み込み

amazon-cognito-identity-jsと、aws-sdk、amazon-cognito-jsを読み込んでいます。
importとrequireが混在していますが、ここではあまり深く考えないことにします。


// See also about the way to load the AmazonCognitoIdentity module.
// https://github.com/aws-amplify/amplify-js/tree/master/packages/amazon-cognito-identity-js#usage

const AmazonCognitoIdentity = require('amazon-cognito-identity-js');
import { CognitoUserPool, CognitoUserAttribute, CognitoUser } from 'amazon-cognito-identity-js';

const AWS = require('aws-sdk');
require('amazon-cognito-js');

2. グローバル変数定義

まずはグローバルな定数定義です。本稿のPart.1にもREADME.mdにも書いておりますが、Cognito User Pool/Federated Identity PoolのId等が必要となります。

// ------------------------------------------------------------
// Global variables
// ------------------------------------------------------------
const REGION = '** place your region name in string. **';
const POOL_DATA = {
  UserPoolId: '** place your Pool Id. **',
  ClientId: '** place your App Client Id. **'
};
const IDENTITY_POOL_ID = '** place your Cognito Identity Pool Id **';
const LOGINS_KEY = 'cognito-idp.' + REGION + '.amazonaws.com/' + POOL_DATA.UserPoolId;

下記はグローバル変数です。CognitoUserPoolオブジェクトやAccessTokenなど、グローバルに持ちまわるオブジェクトを格納します。
これらは各処理の中で代入されていきます。

var gCognitoUserPool;
var gCognitoUser;
var gAccessToken;
var gIdToken;
var gRefreshToken;

3. ユーティリティ関数定義

function gLoadInputData()

text/passwordフィールドの値を取得して、まとめて1つのオブジェクトとして返却する関数です。
各ボタンを押した際の処理で、各フィールド値を取得するために呼ばれます。

function gPutMessage(message)
function gAppendMessage(message)

HTMLのMessage area(当該エレメントのidはmessage-area)に、文字列を表示させます。両者の関数は、既存の文字列を上書きする/追記する、という違いがあります。

function gInitCognitoUser()

CognitoUserPoolオブジェクト及びCognitoUserオブジェクトを初期化します。これはリロード直後の処理として呼ばれます。
また既にログイン済みであるかどうかを判別し、ログイン済みであればAccessToken/IdToken/RefreshTokenを取得してグローバル変数に代入します。

function gUpdateShowingState()

上記と併せ、リロード直後の処理として呼ばれます。
ログイン済みであればユーザ名や各Tokenの内容を画面に表示させます。

4. Amazon Cognito Identity SDK for JavaScriptの各機能を実現する関数定義(※これがメイン)

各ボタンに対応した関数を1つずつ定義しています。詳細は後述します。

// ------------------------------------------------------------
// Functions to be associated with each button.
// ------------------------------------------------------------
function userSignUp() {
// ** omit **
};

function verifyCode(){
// ** omit **
};

function resendConfirmationByEmail(){
// ** omit **
};

function resetPassword(){
};

function confirmPassword(){
// ** omit **
};

function loginUser(){
// ** omit **
};

function getAWSCredentials(){
// ** omit **
};

function logoutUser(){
// ** omit **
};

// ** omit **

5. 上記項番4の関数をHTMLの各ボタンにaddEventListenerする処理

特に説明はいらないと思いますが、各ボタンに対してaddEventListenerで関数を紐づけています。
addEventListenerの引数に無名関数を渡すと読みにくくなったのでこうしています。

// ** omit **

// ------------------------------------------------------------
// associates each functionalities to each button.
// ------------------------------------------------------------
document.getElementById("sign-up-button").addEventListener("click", userSignUp);
document.getElementById("verify-code-button").addEventListener("click", verifyCode);
document.getElementById("resend-confirmation-by-email-button").addEventListener("click", resendConfirmationByEmail);
document.getElementById("reset-password-button").addEventListener("click", resetPassword);
document.getElementById("confirm-password-button").addEventListener("click", confirmPassword);
document.getElementById("login-button").addEventListener("click", loginUser);
document.getElementById("get-aws-credentials-button").addEventListener("click", getAWSCredentials);
document.getElementById("logout-button").addEventListener("click", logoutUser);

// ** omit **

6. リロード直後の初期化処理(ログインセッションがあれば読み出す処理など)

項番3で見た、ユーティリティ関数を呼ぶだけです。

// ** omit **

// ------------------------------------------------------------
// main processes
// ------------------------------------------------------------

gInitCognitoUser();
gUpdateShowingState();

各機能の解説

以上で、index.jsの大まかな構造が分かりました。
それでは、Cognito User Pool/Federated Identity Poolに関する処理を一つ一つ見ていきましょう。

function userSignUp()

基本的に、下記README.mdのUsageのUse case 1に従って実装しています。
https://github.com/aws-amplify/amplify-js/tree/master/packages/amazon-cognito-identity-js#usage

Use case 1. Registering a user with the application. One needs to create a CognitoUserPool object by providing a UserPoolId and a ClientId and signing up by using a username, password, attribute list, and validation data.

ポイントは下記の通りです。

  • CognitoUserPool.signUp()関数で、ユーザの登録が行われる。
  • 上記関数は以下の5つの引数を取る。
    • ユーザ名
    • パスワード
    • attributeList(emailアドレスやphone_numberなど、Cognito User PoolでAttributesとして管理されるもの)
    • ※第4引数は詳細不明。ドキュメント上、「validation data」と書いてあるが…
    • 処理完了時に呼ばれるコールバック関数。エラーの場合も正常終了の場合も呼ばれる。
  • attributeListに追加する項目は、AmazonCognitoIdentity.CognitoUserAttributeを元に生成する。
function userSignUp() {
  console.log("A function " + userSignUp.name + " has started.");
  gPutMessage("");

  const inputData = gLoadInputData();
  console.log("inputData: " + JSON.stringify(inputData));

  // validate input values
  // ** omit **

  const userAttributeList = [];
  const dataEmail = {
    Name: 'email',
    Value: inputData.email
  };

  const attributeEmail = new AmazonCognitoIdentity.CognitoUserAttribute(dataEmail);

  userAttributeList.push(attributeEmail);

  gCognitoUserPool.signUp(inputData.username, inputData.password, userAttributeList, null, function(err, result) {
    console.log("err: ", err);
    console.log("result: ", result);

    // Registration failed.
    if(err){
      gPutMessage('User registration error: Cognito has returned the message as follows: \n' + JSON.stringify(err));
      alert("sorry, your try to sign up has failed.");
      return;
    }

    // Registration succeeded.
    gCognitoUser = result.user;

    console.log('registered user name is ' + gCognitoUser.getUsername());
    gPutMessage('User registration has finished successfully.\n' + 'Registered user name is ' + gCognitoUser.getUsername());
  });

  console.log("A function " + userSignUp.name + " has finished.");
};

function verifyCode()

基本的に、下記README.mdのUsageのUse case 2に従って実装しています。
https://github.com/aws-amplify/amplify-js/tree/master/packages/amazon-cognito-identity-js#usage

Use case 2. Confirming a registered, unauthenticated user using a confirmation code received via SMS.

ポイントは下記の通りです。

  • CognitoUser.confirmRegistration()関数で、ユーザのConfirmationが行われる。
  • 上記関数は以下の引数を取る
    • Verification Code(SMS/電子メールに記載されているもの)
    • ※第2引数の詳細は不明
    • 処理完了時に呼ばれるコールバック関数。エラーの場合も正常終了の場合も呼ばれる。
  • VerificationCodeは6桁の数字。SMSで送信する場合でも電子メールで送信する場合でも。
function verifyCode(){
  console.log("A function " + verifyCode.name + " has started.");
  gPutMessage("");
  const inputData = gLoadInputData();
  console.log("inputData: " + JSON.stringify(inputData));

  const userData = {
    Username: inputData.username,
    Pool: gCognitoUserPool
  };

  const cognitoUser = new AmazonCognitoIdentity.CognitoUser(userData);

  cognitoUser.confirmRegistration(inputData.verificationCode, true, function(err, result) {
    console.log("err: ", err);
    console.log("result: ", result);
    if (err) {
      gPutMessage('User verification error: Cognito has returned the message as follows: \n' + JSON.stringify(err));
      alert("sorry, verification has failed.");
      return;
    };
  });

  console.log("A function " + verifyCode.name + " has finished.");
};

function resendConfirmationByEmail()

基本的に、下記README.mdのUsageのUse case 3に従って実装しています。
https://github.com/aws-amplify/amplify-js/tree/master/packages/amazon-cognito-identity-js#usage

Use case 3. Resending a confirmation code via SMS for confirming registration for a unauthenticated user.

ポイントは下記の通りです。

  • CognitoUser.resendConfirmationCode()関数により、Verification Codeの再送が行われる。
  • 上記関数でとる引数は、処理完了時に呼ばれるコールバック関数のみ。
  • CognitoUserオブジェクト生成時にuserDataオブジェクトを渡しており、ここにユーザ名が入っているため、上記関数は他に引数が無いものと推測できる。
function resendConfirmationByEmail(){
  console.log("A function " + resendConfirmationByEmail.name + " has started.");
  gPutMessage("");
  const inputData = gLoadInputData();
  console.log("inputData: " + JSON.stringify(inputData));

  const userData = {
    Username: inputData.username,
    Pool: gCognitoUserPool
  };

  const cognitoUser = new AmazonCognitoIdentity.CognitoUser(userData);

  cognitoUser.resendConfirmationCode(function(err, result) {
    console.log("err: ", err);
    console.log("result: ", result);
    if (err) {
      gPutMessage('Resending confirmation code error: Cognito has returned the message as follows: \n' + JSON.stringify(err));
      alert("sorry, resending confirmation has failed.");
      return;
    };
  });

  console.log("A function " + resendConfirmationByEmail.name + " has finished.");
};

function resetPassword()

基本的に、下記README.mdのUsageのUse case 12に従って実装しています。
https://github.com/aws-amplify/amplify-js/tree/master/packages/amazon-cognito-identity-js#usage

Use case 12. Starting and completing a forgot password flow for an unauthenticated user.

ポイントは下記の通りです。

  • CognitoUser.forgotPassword()関数により、パスワードが初期化され、Verification Codeが送信される。
  • 上記関数でとる引数は、種々のコールバック関数を持ったオブジェクト1つ。
  • 上記オブジェクトは、少なくとも下記3つの関数を指定できる。他にもあるかもしれないが詳細は未確認。
    • onSuccess
    • onFailure
    • inputVerificationCode
  • 上記で指定するinputVerificationCode関数の中でCognitoUser.confirmPassword()関数を実行するよう実装することが出来る。
  • CognitoUser.confirmPassword()は以下の3つの引数を取る。
    • verificationCode
    • newPassword
    • コールバック関数をまとめたオブジェクト
      • onSuccess
      • onFailure
function resetPassword(){
  console.log("A function " + resetPassword.name + " has started.");
  gPutMessage("");
  const inputData = gLoadInputData();
  console.log("inputData: " + JSON.stringify(inputData));

  const userData = {
    Username: inputData.username,
    Pool: gCognitoUserPool
  };

  const cognitoUser = new AmazonCognitoIdentity.CognitoUser(userData);

  cognitoUser.forgotPassword({
    onSuccess: (data) => {
      console.log(data);
      gPutMessage("Your password needs resetting and confirming.");
      gAppendMessage("data: " + JSON.stringify(data));
    },
    onFailure: (err) => {
      console.log(err);
      gPutMessage("Resetting password failed.");
      gAppendMessage("err: " + JSON.stringify(err));
    },
    inputVerificationCode: (data) => {
      console.log("Code sent to:" + data);
      var verificationCode = prompt('Please input verification code ', '');
      var newPassword = prompt('Enter new password ', '');
      cognitoUser.confirmPassword(verificationCode, newPassword, {
        onSuccess: () => {
          console.log('Password confirmed!');
          gAppendMessage('Resetting password succeeded!');
        },
        onFailure: (err) => {
          console.log('Password not confirmed!');
          gAppendMessage('Resetting password failed.');
        }
      });
    }
  });

  console.log("A function " + resetPassword.name + " has finished.");
};

function confirmPassword()

上記のresetPassword()処理から、CognitoUser.confirmPassword()だけを個別にとりだしたものとなります。

先ほどのresetPassword()の実装の場合、Verification Codeのメールを送信した後にポップアップウィンドウを2つ表示させて、ここにVerification Codeと新しいパスワードを入力させるようになっています。
ここで入力を間違えると再度Verification Codeのメールの再送が行われ、メールボックスが鬱陶しいことになるので、敢えてCognitoUser.confirmPassword()だけを取り出しています。

function confirmPassword(){
  console.log("A function " + resetPassword.name + " has started.");
  gPutMessage("");
  const inputData = gLoadInputData();
  console.log("inputData: " + JSON.stringify(inputData));

  const userData = {
    Username: inputData.username,
    Pool: gCognitoUserPool
  };

  const cognitoUser = new AmazonCognitoIdentity.CognitoUser(userData);
  cognitoUser.confirmPassword(inputData.verificationCode, inputData.password, {
    onSuccess: () => {
      console.log('Password confirmed!');
      gAppendMessage('Resetting your password succeeded!');
    },
    onFailure: (err) => {
      console.log('Password not confirmed!');
      gAppendMessage('Confirming your verification code or new password failed.');
    }
  });

};

function loginUser()

基本的に、下記README.mdのUsageのUse case 4に従って実装しています。
https://github.com/aws-amplify/amplify-js/tree/master/packages/amazon-cognito-identity-js#usage

Use case 4. Authenticating a user and establishing a user session with the Amazon Cognito Identity service.

ポイントは下記の通りです。

  • CognitoUser.authenticateUser()関数で、ユーザの認証を行う。
  • 上記関数でとる引数は下記の通り。
    • authenticationDetails(AmazonCognitoIdentity.AuthenticationDetailsから生成する)
    • コールバック関数をまとめたオブジェクト
      • onSuccess
      • onFailure
function loginUser(){
  console.log("A function " + loginUser.name + " has started.");
  gPutMessage("");
  const inputData = gLoadInputData();
  console.log("inputData: " + JSON.stringify(inputData));


  const authenticationData = {
    Username: inputData.username,
    Password: inputData.password
  };
  const authenticationDetails = new AmazonCognitoIdentity.AuthenticationDetails(authenticationData);
  const userData = {
    Username: inputData.username,
    Pool: gCognitoUserPool
  };

  // override the global variable "gCognitoUser" object.
  gCognitoUser = new AmazonCognitoIdentity.CognitoUser(userData);

  gCognitoUser.authenticateUser(authenticationDetails, {
    onSuccess: (result) => {
      console.log("result: ", result);
      console.log("gCognitoUser: ", gCognitoUser);
      const accessToken = result.getAccessToken().getJwtToken();
      gPutMessage("Login succeeded!\n");
      gAppendMessage("\naccessToken: " + accessToken);

     gInitCognitoUser();
    },
    onFailure: (err) => {
      console.log("err: ", err);
      gPutMessage("\n" + "login failed.\n");
      gAppendMessage("err: " + JSON.stringify(err));
    },
  });

  console.log("A function " + loginUser.name + " has finished.");
};

function getAWSCredentials()

基本的に、下記README.mdのUsageのUse case 17に従って実装しています。
https://github.com/aws-amplify/amplify-js/tree/master/packages/amazon-cognito-identity-js#usage

Use case 17. Integrating User Pools with Cognito Identity.

ポイントは下記の通りです。

  • CognitoUserオブジェクトから、getSession()関数で現在のログインセッション情報を取り出す。
  • 有効なログインセッションから、IdTokenを取り出し、Cognito Federated Identity Poolと連携させる
  • Cognito Federated Identity Poolの利用は、amazon-cognito-jsが必要(※下記参照)
  • AWS.CognitoIdentityCredentialsオブジェクトの生成時に、Cognito User PoolのIdTokenとその他付帯情報を渡す(下記コードではcognitoIdentityParams変数)。
  • cognitoIdentityParamsは下記の構造を持つ。これは、GetOpenIdTokenForDeveloperIdentity APIのLoginsパラメータと同様の構造を持つ(※下記参照)
  • AWS.CognitoIdentityCredentialsオブジェクトのrefresh()関数を実行することで、Federated Identitiesの連携が行われる。成功すると、accessKeyId/SecretAccessKey/SessionTokenが更新される。
  • 有効なaccessKeyId/SecretAccessKey/SessionTokenがあり、対応するIAM Roleに適切なPermissionsが付与されていれば、これをもとにAWSリソースを直接操作することが出来る。
amazon-cognito-jsの利用
// amazon-cognito-jsの利用
const AWS = require('aws-sdk');
require('amazon-cognito-js');
cognitoIdentityParamsの構造
{
  IdentityPoolId: "ap-northeast-1:9d83****-****-****-****-************", // Federated Identity PoolのID。User PoolのIDではない。
  Logins: {
    'cognito-idp.ap-northeast-1.amazonaws.com/ap-northeast-1_*********': session.getIdToken().getJwtToken() 
    // キー名にはRegion名及びUser Pool Idが含まれる。キー名を変数の値で指定したい場合は、ES2015の記法を使うのがシンプルで良いと思う。
    // https://qiita.com/kmagai/items/95481a3b9fd97e4616c9 - ES2015以降のJavaScriptでObjectのkeyに変数を使う
  }
}
function getAWSCredentials(){
  console.log("A function " + getAWSCredentials.name + " has started.");
  gPutMessage("");
  const inputData = gLoadInputData();
  console.log("inputData: " + JSON.stringify(inputData));

  // retrieve current session
  gCognitoUser.getSession((err, session) => {
    if (err) {
      console.log("getSession: err: " + JSON.stringify(err));
      return;
    }
    console.log('session validity: ' + session.isValid());
    // console.log('session: ' + JSON.stringify(session));


    // Prepare an object corresponds to the Cognito Identity Pool
    const cognitoIdentityParams = {
      IdentityPoolId: IDENTITY_POOL_ID,
      Logins: {
        [LOGINS_KEY]: session.getIdToken().getJwtToken()
      }
    };
    console.log(cognitoIdentityParams);

    AWS.config.region = REGION;
    AWS.config.credentials = new AWS.CognitoIdentityCredentials(cognitoIdentityParams);

    // refresh AWS credentials
    AWS.config.credentials.refresh((err) => {
      if (err){
        console.log(err);
        gAppendMessage("\n" + "refreshing AWS credentials failed. Cognito Identity returns messages as follows: \n" + JSON.stringify(err));
        return;
      }

      gAppendMessage("\n" + "refreshing has succeeded.");

      const identityId = AWS.config.credentials.identityId;
      const credentials = AWS.config.credentials;
      console.log("identityId: " + identityId);
      console.log(AWS.config.credentials)
      gAppendMessage("\n" + "identityId: " + identityId);
      gAppendMessage("\n" + "accessKeyId: " + credentials.accessKeyId);
      gAppendMessage("\n" + "secretAccessKey: " + credentials.secretAccessKey);
      gAppendMessage("\n" + "sessionToken: " + credentials.sessionToken);
    });
  });

  console.log("A function " + getAWSCredentials.name + " has finished.");
};

function logoutUser()

下記README.mdのUsageのUse case 14に従って実装しています。
https://github.com/aws-amplify/amplify-js/tree/master/packages/amazon-cognito-identity-js#usage

Use case 14. Signing out from the application.

特筆するべき事項は無いかと思います。

function logoutUser(){
  gCognitoUser.signOut();
  // ** omit **
};

おわりに

CognitoをWebアプリから利用するために必要となる処理を一通り実装してきましたが、基本的には公式のREADME.md通りの実装で特に問題なく実現できました。
https://github.com/aws-amplify/amplify-js/tree/master/packages/amazon-cognito-identity-js

ただ、いずれも少し説明不足で、理解するためには実際に色々と書いてみる必要があるかと思います。特に、Federated Identitiesとの連携はハマりポイントが多いと思います。
本稿がJavaScriptからCognito User Pool/Identity Poolを利用するための一助となれば幸いです。

しかし、Promiseを使わない昔ながらのコールバック関数を利用したやり方は、少し処理が増えるとやはり読みにくくなりますね。
Amplify JSだとPromise使っているようなので、やっぱりこちらを利用するのが良いと思います。
https://aws-amplify.github.io/docs/js/authentication#sign-in

42
35
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
42
35