ElixirDesktopで作るスマホアプリ Part 11 ログアウト、ユーザー削除、利用規約などのアプリ全体の設定

# はじめに

この記事はElixirアドベントカレンダー2024のシリーズ２、14日目の記事です

https://qiita.com/advent-calendar/2024/elixir

アプリ全体の設定を作成していきます

# 作る機能

1. UIに使うテキストの辞書データ作成
1. 設定一覧画面を作成
2. ユーザー設定を配下に追加
2. お問い合わせを配下に追加
2. プライバシーポリシーを配下に追加
2. 利用規約を配下に追加
2. ユーザー削除ボタンを追加
2. ログアウトボタンを追加

# 辞書データを作成

今回使う文言を末尾に追加

```bash:priv/gettext/default.pot:L186
# Setting

msgid "General"
msgstr ""

msgid "Privacy Policy"
msgstr ""

msgid "Terms of Service"
msgstr ""

msgid "Delete Account"
msgstr ""

msgid "Sign out"
msgstr ""

msgid "Setting"
msgstr ""

msgid "Contact"
msgstr ""
```

以下のコマンドを実行して他の辞書に反映します

```
mix gettext.merge priv/gettext
```

日本語テキストを追加

```bash:priv/gettext/ja/LC_MESSAGES/default.po
# Setting

msgid "General"
msgstr "ユーザー設定"

msgid "Privacy Policy"
msgstr "プライバシーポリシー"

msgid "Terms of Service"
msgstr "利用規約"

msgid "Delete Account"
msgstr "アカウント削除"

msgid "Sign out"
msgstr "ログアウト"

msgid "Setting"
msgstr "設定"

msgid "Contact"
msgstr "問い合わせ"

```

# 設定一覧画面を作成



ベースとなるページを作成

```elixir:lib/trarecord_web/live/setting_live/index.ex
defmodule TrarecordWeb.SettingsLive.Index do
  use TrarecordWeb, :live_view

  @impl true
  def render(assigns) do
    ~H"""
    <.header_nav title={gettext("Setting")} />
    <div id="settings" class="h-screen mt-20 flex flex-col gap-y-8 mx-2">
      <table class="table bg-white text-base">
        <tbody>
          <tr>
            <td>
              <.link class="flex justify-between" navigate={~p"/users/settings"}>
                <p><%= gettext("General") %></p>
                <.icon name="hero-chevron-right-solid" />
              </.link>
            </td>
          </tr>
        </tbody>
      </table>
    </div>
    <.bottom_tab current="Setting" />
    """
  end

  @impl true
  def mount(_params, session, socket) do
    Gettext.put_locale(TrarecordWeb.Gettext, Map.get(session, "locale", "ja"))

    {:ok, socket}
  end
end
```

ルートに追加

```diff_elixir:lib/trarecord_web/router.ex
  scope "/", TrarecordWeb do
    pipe_through [:browser, :require_authenticated_user]

    live_session :require_authenticated_user,
      on_mount: [{TrarecordWeb.UserAuth, :ensure_authenticated}] do
+     live "/settings", SettingsLive, :infex
      live "/users/settings", UserSettingsLive, :edit
      live "/users/settings/confirm_email/:token", UserSettingsLive, :confirm_email
      live "/onboarding", OnboardingLive.Index, :index

      live "/folders", FolderLive.Index, :index
      live "/folders/new", FolderLive.Index, :new
      live "/folders/:id/edit", FolderLive.Index, :edit
      live "/folders/:id/delete", FolderLive.Index, :delete

      live "/folders/:id", FolderLive.Show, :show
      live "/folders/:id/show/edit", FolderLive.Show, :edit
    end
  end
```

ナビゲーションリンクの差し替え

```diff_elixir:lib/trarecord_web/components/navigation.ex:L50
  defp links() do
    [
      {"Folder", "hero-book-open-solid", "/folders"},
-     {"Setting", "hero-cog-6-tooth-solid", "/users/settings"}
+     {"Setting", "hero-cog-6-tooth-solid", "/settings"}

    ]
  end
```

![スクリーンショット 2024-12-30 14.26.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/e1cf3ea2-292e-1ae9-0fd9-29572a2675b4.png)

# ユーザー設定を配下に追加

導線・ページ自体は作ってあるので、戻るボタンを表示します

```diff_elixir:lib/trarecord_web/live/user_settings_live.ex
defmodule TrarecordWeb.UserSettingsLive do
  use TrarecordWeb, :live_view

  alias Trarecord.Accounts

  def render(assigns) do
    ~H"""
-   <.header_nav title={gettext("Setting")} />
+   <.header_nav title={gettext("Setting")}>
+     <:back>
+       <.link navigate={~p"/settings"}>
+         <%= gettext("Back") %>
+       </.link>
+     </:back>
+   </.header_nav>
    ...
    """
  end

-  def mount(%{"token" => token}, _session, socket) do
+  def mount(%{"token" => token}, session, socket) do
+   Gettext.put_locale(TrarecordWeb.Gettext, Map.get(session, "locale", "ja"))
    socket =
      case Accounts.update_user_email(socket.assigns.current_user, token) do
        :ok ->
          put_flash(socket, :info, gettext("Email changed successfully."))

        :error ->
          put_flash(socket, :error, "Email change link is invalid or it has expired.")
      end

    {:ok, push_navigate(socket, to: ~p"/users/settings")}
  end

- def mount(_params, _session, socket) do
+ def mount(_params, session, socket) do
+   Gettext.put_locale(TrarecordWeb.Gettext, Map.get(session, "locale", "ja"))
    user = socket.assigns.current_user
    email_changeset = Accounts.change_user_email(user)
    password_changeset = Accounts.change_user_password(user)

    socket =
      socket
      |> assign(:current_password, nil)
      |> assign(:email_form_current_password, nil)
      |> assign(:current_email, user.email)
      |> assign(:email_form, to_form(email_changeset))
      |> assign(:password_form, to_form(password_changeset))
      |> assign(:trigger_submit, false)

    {:ok, socket}
  end
  ...
end
```

![スクリーンショット 2024-12-30 14.37.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/3a2fcbcc-0bbf-6586-1f94-54c2361e606c.png)

2. お問い合わせを配下に追加

お問い合わせはgoogle formが楽なのでそちらのURLを設置します

open_safariのnative bridgeを使用して開くのでphx-hookをセットしてクリックイベントを追加します

```diff_elixir:lib/trarecord_web/live/setting_live/index.ex
defmodule TrarecordWeb.SettingLive.Index do
  use TrarecordWeb, :live_view

  @contact_form [goole form url]

  def render(assigns) do
    ~H"""
    <.header_nav title={gettext("Setting")} />
-   <div id="settings" class="h-screen my-20 flex flex-col gap-y-8 mx-2">
+   <div id="settings" class="h-screen my-20 flex flex-col gap-y-8 mx-2" phx-hook="NativeBridge">

      <table class="table bg-white text-base">
        <tbody>
          <tr>
            <td>
              <.link class="flex justify-between" navigate={~p"/users/settings"}>
                <p><%= gettext("General") %></p>
                <.icon name="hero-chevron-right-solid" />
              </.link>
            </td>
          </tr>
        </tbody>
      </table>
+     <table class="table bg-white text-base">
+       <tbody>
+         <tr>
+           <td>
+             <a
+               href={@contact_form}
+               target="_blank"
+               class="flex justify-between"
+               phx-click="open_contact"
+             >
+               <p><%= gettext("Contact") %></p>
+               <.icon name="hero-chevron-right-solid" />
+             </a>
+           </td>
+         </tr>
+       </tbody>
+     </table>
    </div>
    <.bottom_tab current="Setting" />
    """
  end

  @impl true
  def mount(_params, session, socket) do
    Gettext.put_locale(TrarecordWeb.Gettext, Map.get(session, "locale", "ja"))

-   {:ok, socket}
+   {:ok, assign(socket, :contact_form, @contact_form)}
  end

+ @impl true 
+ def handle_event("open_contact", _params, socket) do
+   {:noreply, push_event(socket, "open_safari", %{url: @contact_form})}
+ end
end  
```

![スクリーンショット 2024-12-30 15.16.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/26b5173c-2018-819b-cd15-860722feb47b.png)

# 利用規約、プライバシーポリシーを配下に追加

```diff_elixir:lib/trarecord_web/live/setting_live/index.ex:L7
  def render(assigns) do
    ~H"""
    <.header_nav title={gettext("Setting")} />
    <div id="settings" class="h-screen my-20 flex flex-col gap-y-8 mx-2" phx-hook="NativeBridge">
      <table class="table bg-white text-base">
        ...
      </table>
      <table class="table bg-white text-base">
        ...
      </table>
+     <table class="table bg-white text-base">
+       <tbody>
+         <tr>
+           <td>
+             <.link class="flex justify-between" navigate={~p"/settings/gp"}>
+               <p><%= gettext("Terms of Service") %></p>
+               <.icon name="hero-chevron-right-solid" />
+             </.link>
+           </td>
+         </tr>
+         <tr>
+           <td>
+             <.link class="flex justify-between" navigate={~p"/settings/privacy"}>
+               <p><%= gettext("Privacy Policy") %></p>
+               <.icon name="hero-chevron-right-solid" />
+             </.link>
+           </td>
+         </tr>
+       </tbody>
+     </table>
    </div>
    <.bottom_tab current="Setting" />
    """
  end
```

利用規約のページを作ります

```lib/trarecord_web/live/setting_live/gp.ex
defmodule TrarecordWeb.SettingsLive.Gp do
  use TrarecordWeb, :live_view

  def render(assigns) do
    ~H"""
    <.header_nav title={gettext("Terms of Service")}>
      <:back>
        <.link navigate={~p"/settings"}>
          <.icon name="hero-chevron-left-solid" class="h-6 w-6" />
        </.link>
      </:back>
    </.header_nav>
    <div class="flex flex-col gap-y-2 p-4 bg-white mb-20">
      <h2 class="text-xl">利用規約</h2>
      <p>
        この利用規約（以下，「本規約」といいます。）は，Trarecord（以下，「当社」といいます。）がこのウェブ上で提供するサービス（以下，「本サービス」といいます。）の利用条件を定めるものです。登録ユーザーの皆さま（以下，「ユーザー」といいます。）には，本規約に従って，本サービスをご利用いただきます。
      </p>
      
      ここに規約を書く

      <p class="tR">以上</p>
    </div>
    <.bottom_tab current="Setting" />
    """
  end

  def mount(_params, session, socket) do
    Gettext.put_locale(TrarecoWeb.Gettext, Map.get(session, "locale", "en"))
    {:ok, socket}
  end
end
```

プライバシーポリシーのページを作ります

```lib/trarecord_web/live/setting_live/privacy.ex
defmodule TrarecoWeb.SettingsLive.Privacy do
  use TrarecordWeb, :live_view

  def render(assigns) do
    ~H"""
    <.header_nav title={gettext("Privacy Policy")}>
      <:back>
        <.link navigate={~p"/settings"}>
          <.icon name="hero-chevron-left-solid" class="h-6 w-6" />
        </.link>
      </:back>
    </.header_nav>
    <div class="flex flex-col gap-y-2 p-4 bg-white mb-20">
      <h2 class="text-xl">プライバシーポリシー</h2>
      <p>
        Trareco（以下，「当社」といいます。）は，本ウェブ上で提供するサービス（以下,「本サービス」といいます。）における，ユーザーの個人情報の取扱いについて，以下のとおりプライバシーポリシー（以下，「本ポリシー」といいます。）を定めます。
      </p>
      ここにプライバシーポリシーを書く
      <p class="tR">以上</p>
    </div>
    <.bottom_tab current="Setting" />
    """
  end

  def mount(_params, session, socket) do
    Gettext.put_locale(TrarecoWeb.Gettext, Map.get(session, "locale", "en"))
    {:ok, socket}
  end
end
```

ルーターに追加して完了です

```diff_elixir:lib/trarecord_web/router.ex
  scope "/", TrarecordWeb do
    pipe_through [:browser, :require_authenticated_user]

    live_session :require_authenticated_user,
      on_mount: [{TrarecordWeb.UserAuth, :ensure_authenticated}] do
      live "/settings", SettingLive.Index, :infex
+     live "/settings/gp", SettingsLive.Gp
+     live "/settings/privacy", SettingsLive.Privacy

      live "/users/settings", UserSettingsLive, :edit
      live "/users/settings/confirm_email/:token", UserSettingsLive, :confirm_email
      live "/onboarding", OnboardingLive.Index, :index

      live "/folders", FolderLive.Index, :index
      live "/folders/new", FolderLive.Index, :new
      live "/folders/:id/edit", FolderLive.Index, :edit
      live "/folders/:id/delete", FolderLive.Index, :delete

      live "/folders/:id", FolderLive.Show, :show
      live "/folders/:id/show/edit", FolderLive.Show, :edit
    end
  end
```

# ユーザー削除ボタンを追加

削除関数を追加

```diff_elixir:lib/trarecord/accounts.ex:L61
  def get_user!(id), do: Repo.get!(User, id)

+ def delete_user(user), do: Repo.delete(user)   
```

削除確認モーダルと削除イベントを作ります

```diff_elixir:lib/trarecord_web/live/setting_live/index.ex
+ alias Trarecord.Accounts

  @impl true
  def render(assigns) do
    ~H"""
    <.header_nav title={gettext("Setting")} />
    <div id="settings" class="h-screen my-20 flex flex-col gap-y-8 mx-2" phx-hook="NativeBridge">
      <table class="table bg-white text-base">
      ...
      </table>
+     <.link patch={~p"/settings/delete"} class="btn btn-error text-white normal-case text-xl">
+       <%= gettext("Delete Account") %>
+     </.link>
    </div>
    <.bottom_tab current="Setting" />
+   <.modal
+     :if={@live_action in [:delete]}
+     id="user-delete-modal"
+     show
+     on_cancel={JS.navigate(~p"/settings")}
+   >
+     <p class="m-8"><%= gettext("Are you sure?") %></p>
+     <div class="flex justify-between mx-4 gap-x-4">
+       <button class="btn w-28" phx-click={JS.navigate(~p"/settings")}>
+         <%= gettext("Cancel") %>
+       </button>
+       <button
+         class="btn btn-error text-white w-28"
+         phx-click={JS.push("delete", value: %{id: @current_user.id})}
+       >
+         <%= gettext("Delete") %>
+       </button>
+     </div>
+   </.modal>
    """
  end

  @impl true
  def mount(_params, session, socket) do
    Gettext.put_locale(TrarecordWeb.Gettext, Map.get(session, "locale", "ja"))

    {:ok, assign(socket, :contact_form, @contact_form)}
  end

+ @impl true
+ def handle_params(params, _url, socket) do
+   {:noreply, apply_action(socket, socket.assigns.live_action, params)}
+ end

+ defp apply_action(socket, :delete, _params) do
+   socket
+   |> assign(:page_title, "Delete Account")
+ end

+ defp apply_action(socket, _action, _params), do: socket

  @impl true
  def handle_event("open_contact", _params, socket) do
    {:noreply, push_event(socket, "open_safari", %{url: @contact_form})}
  end
  
+ def handle_event("delete", %{"id" => id}, socket) do
+   user = Accounts.get_user!(id)
+    case Accounts.delete_user(user) do
+     {:ok, _} ->
+       File.rm(Trareco.config_dir() <> "/token")
+        {
+         :noreply,
+         socket
+         |> put_flash(:info, "Account deleted successfully")
+         |> push_navigate(to: ~p"/")
+       }
+
+     {:error, _} ->
+       {
+         :noreply,
+         socket
+         |> put_flash(:error, "Account deleted error")
+       }
+   end
  end
end 
```

routerに削除モーダル表示用のパスを追加

```diff_elixir:lib/trarecord_web/router.ex
  scope "/", TrarecordWeb do
    pipe_through [:browser, :require_authenticated_user]

    live_session :require_authenticated_user,
      on_mount: [{TrarecordWeb.UserAuth, :ensure_authenticated}] do
      live "/settings", SettingLive.Index, :infex
+     live "/settings/delete", SettingsLive.Index, :delete      
      live "/settings/gp", SettingsLive.Gp
      live "/settings/privacy", SettingsLive.Privacy

      live "/users/settings", UserSettingsLive, :edit
      live "/users/settings/confirm_email/:token", UserSettingsLive, :confirm_email
      live "/onboarding", OnboardingLive.Index, :index

      live "/folders", FolderLive.Index, :index
      live "/folders/new", FolderLive.Index, :new
      live "/folders/:id/edit", FolderLive.Index, :edit
      live "/folders/:id/delete", FolderLive.Index, :delete

      live "/folders/:id", FolderLive.Show, :show
      live "/folders/:id/show/edit", FolderLive.Show, :edit
    end
  end
```

# ログアウトボタンを追加

リンクとログアウト機能自体はあるのでリンクを設置して完了です

```diff_elixir:lib/trarecord_web/live/setting_live/index.ex:L61
      <.link patch={~p"/settings/delete"} class="btn btn-error text-white normal-case text-xl">
        <%= gettext("Delete Account") %>
      </.link>
+     <.link
+       method="delete"
+       href="/users/log_out"
+       class="btn btn-error text-white normal-case text-xl"
+     >
+       <%= gettext("Sign out") %>
+     </.link>
```

![スクリーンショット 2024-12-31 22.48.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/b3a3af17-3098-df02-15bc-b1362fe28191.png)

# 最後に

全体の設定画面を作成して、問い合わせフォーム、利用規約、プライバシーポリシー等のアプリの運用上必要なものの設置

ログアウト、ユーザー削除を実装しました

次はGoogle Places APIからスポット情報を取得する方法を解説します

本記事は以上になりますありがとうございました