5
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

[作ってみた]招待コードを画面上で入力して相互フォロー(夫婦・カップル向けアプリに)

Last updated at Posted at 2021-10-03

概要

個人開発として「夫婦・カップル専用の家計簿WEBアプリ」をRailsで作成しました。
「その際、”夫婦やカップル(以下、夫婦)の相互フォロー機能”をどのように実装するか?」にかなり悩みました。
参考記事も見つからなかったので、私なりの実装方法を記事として残してみようと思っています。

使用技術

フロントエンド
HTML / CSS / Javascript / Bootstrap 5.1.0

バックエンド
Ruby 2.7.4 / Ruby on Rails 6.1.4 / RSpec

インフラ・開発環境
Docker / Docker-compode / CircleCI(CI/CD) / Heroku / SendGrid(mailer)

実装の方針

前提

  • ユーザーの登録にはEmailとパスワードを使います。
  • 相互フォローをすることを前提とします。
    ツイッターのような非対称なフォロー(「自分がフォローした人」と「自分をフォローした人」とが不一致になりえるような形)は想定していません。
    つまり、Relations#new/createのようなアクションを用意し、
    このアクションでを実行することで自分とパートナー2人を一つのRelationインスタンスとして保存します。
  • ユーザーには「夫婦の機能にフォーカスしてもらう」ため、
    パートナー以外とのユーザー同士の交流をすることは考えません。
    そのためUsers#indexのようなアクション/ビューは作りません。

以上の前提から、選択肢が2つ考えられました。

選択肢①(招待メールパターン):

自分のみがまずユーザー登録をする
自分がアプリ上でパートナーのアドレスを入力してパートナーに招待メールを送信
パートナーが招待メールのリンクを踏む
パートナーがそのリンク先からユーザー登録をすることで相互フォローアクションを実行

選択肢②(招待コードパターン):

自分とパートナーが各々ユーザー登録をする
パートナーがアプリ上で招待コードを発行
自分がアプリ上でパートナーの招待コードを入力
相互フォローアクションを実行

→今回は選択肢②を採用

(理由)

  • 選択肢①の場合、パートナーが誤って「招待メールのリンク以外からユーザー登録をすること」がありうる。その場合、相互フォローが実行されない。2人がユーザー削除をして、再度登録作業からやらないといけない
  • 選択肢①の場合、パートナーのメールアドレスを誤って記入する等により、本アプリの名前入りで不特定多数にメールが送られてしまう可能性がある。
  • 選択肢②の場合は、上記の懸念が払しょくされる。
  • 選択肢②の場合は、ユーザーが相互フォロー機能が実行される瞬間をアプリ上で見ることになるので、リッチなアニメーションなどを用いて、ユーザーのエンゲージメント向上のための工夫を加える余地ができる

#実装の流れ

ER図

今回は下図の User, User_Relationship, Relationshipの部分が対象です。

ER.png

アウトライン

①招待コード作成
②相互フォロー実行

今回関係するrouteは以下の通り

routes.rb
  get    '/relationships/invitation_code', to: "relationships#invitation_code"
  resources :relationships,       only: [:new, :create] 

①招待コード作成

招待コードはinvitation_token、招待コードをハッシュ化しでDB保存したものをinvitation_digestとします。
before_actionフィルターを用いて、
"relationships/invitation_code.html"に遷移するごとに、Userモデルのinvitation_digestを更新します。
(招待コードを更新する仕様にすることで、招待コードの流出による被害を防止)

controller/relationships.rb
  before_action :create_invitation_digest, only: [:invitation_code]

  def invitation_code
  end

  private
    def create_invitation_digest
      current_user.invitation_token = User.new_token
      current_user.attributes = { invitation_digest: User.digest(current_user.invitation_token),
                                  invitation_made_at: Time.zone.now }
      current_user.save(context: :except_password_change)
      # context => modelにて"passwordのpresence: true" となっているバリデーションをskipする
    end

招待コードはreadonlyにし、copyボタンを実装することにより操作性向上。 ワンタップでLINEで共有できる機能を付けるともっとよさそう…。
relationship/invitation_code.html.erb

    <div class="invitation-code-display">
      <input class="form-control" id="copyTarget" type="text" value="<%= current_user.invitation_token %>" readonly>
      <i class="btn btn-outline-secondary far fa-copy copy-btn" type="button" onclick="copyToClipboard()"></i>
    </div>

    <script>
      function copyToClipboard() {
        var copyTarget = document.getElementById("copyTarget");
        copyTarget.select();
        document.execCommand("Copy");
        alert('コピーしました');
      }
    </script>

    <div class="container">
      <p>こちらの招待コードをパートナーの方の招待コード入力欄に入力してください。</p>
      <p>招待コードはこのURLに訪問したり、再読み込みをするたびに変わります。<br>忘れずにコピーをしてください。</p>
    </div>

②相互フォロー実行

パートナーのアドレス(ユーザーの識別用)、家族名、招待コードを入力できるようにします。

relationship/new.html.erb
    <div class="invitation-code-form">

      <%= form_with(model: @relationship, url: {controller: 'relationships', action: 'create' }, local: true) do |f| %>

      <%= f.label :invitation_code, "パートナーの招待コード:", class: "form-label" %>
      <%= f.text_field :invitation_code , class: "form-control"%>

      <%= f.label :email, "パートナーの登録メールアドレス:", class: "form-label" %>
      <%= f.email_field :email , class: "form-control"%>

      <%= f.label :name, "登録する家族の名前:", class: "form-label" %>
      <%= f.text_field :name , class: "form-control"%>
      <p class="sub-info">6文字以内(例:松田家)</p>

      <%= f.submit "登録" ,class: "btn standerd-btn btn-primary"%>
      <% end %>
    </div>

今回は、自分とパートナーを家族に登録することに加えて、 「共通ユーザー」も作成し、家族に追加しています。 共通ユーザーが不要な方は適宜その点を抜かして読んでください。
controller/relationships.rb

  def new
    @relationship = Relationship.new
  end


  def create
    to_user = User.find_by(email: params[:relationship][:email])
    @relationship = Relationship.new(name: params[:relationship][:name])

  # (1)パートナーのメールアドレスがDBに登録されていることを確認
    if to_user.nil?
      flash[:warning] = "そのメールアドレスのユーザーは登録されていません"
      redirect_to new_relationship_path
  # (2)パートナーがまだ相互フォローしていないことを確認
    elsif !to_user.no_relationship?
      flash[:danger] = "パートナーが既に他の方と家族登録しています"
      redirect_to new_relationship_path
  # (3)入力した招待コードをハッシュ化したものが、DBに保存されているパートナーのinvitation_digestと一致するかを確認
    elsif BCrypt::Password.new(to_user.invitation_digest).is_password?(params[:relationship][:invitation_code])
      # (3-1)入力した params[:relationship]のバリデーションチェック
      if @relationship.save
        # 共通ユーザーでログインはしないが、バリデーションを通すため、パスワードを設定
        common_user_password = SecureRandom.urlsafe_base64(10)
        # 共通ユーザーの作成
        common_user = User.create(name: "共通", 
                                  email: "common_#{current_user.id}@kyodokoza.com", 
                                  password: common_user_password, 
                                  password_confirmation: common_user_password)
        # @relationとcurrent_user/to_user/common_userをつなげる
        current_user.create_user_relationship(relationship_id: @relationship.id)
        to_user.create_user_relationship(relationship_id: @relationship.id)
        common_user.create_user_relationship(relationship_id: @relationship.id)

        flash[:success] = "家族を登録しました"
        redirect_to user_path(current_user)
      # (3-2)入力した params[:relationship][:name]のバリデーションエラー
      else
        flash[:warning] = "家族の名前の文字数を確認してください"
        redirect_to new_relationship_path    
      end
  # (4)入力した招待コードをハッシュ化したものが、DBに保存されているパートナーのinvitation_digestと一致しない
    else
      flash[:warning] = "招待コードが間違っています"
      redirect_to new_relationship_path
    end
  end

さいごに

以上です。
質問や、「ほかにもこんな実装方法があるよ!」などコメントいただけると幸いです。
どなたかのためになればうれしいです。

5
3
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
5
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?