1

I am using devise for authentication. I am overwriting devise token generator so that I can use 6 digit code and also overwriting it so that I can support mobile number confirmation.

If a user register with email and OTP is send via email. Registration seems to work fine. A user register with an email. An OTP is sent and after confirmation a user gets confirmed.

But when the user tries to update the email. I am using the same methods to send the confirmation code (as in registration which works fine) the user get saved in unconfirmed_email. A mail gets send in email but after confirmation a user email is not being copied to email field from unconfirmed_email field.

What could be the problem here.

app/services/users/confirmation_code_sender.rb

# frozen_string_literal: true

module Users
  class ConfirmationCodeSender
    attr_reader :user

    def initialize(id:)
      @user = User.find(id)
    end

    # rubocop :disable Metrics/AbcSize
    def call
      generate_confirmation_token!

      if user.email?
        DeviseMailer.confirmation_instructions(
          user,
          user.confirmation_token,
          { to: user.unconfirmed_email || user.email }
        ).deliver_now
      else
        Telco::Web::Sms.send_text(recipient: user.unconfirmed_mobile || user.mobile_number, message: sms_text)
      end
    end
    # rubocop :enable Metrics/AbcSize

    private

    def generate_confirmation_token!
      user.confirmation_token = TokenGenerator.token(6)
      user.confirmation_sent_at = DateTime.current
      user.save!(validate: false)
    end

    def sms_text
      I18n.t('sms.confirmation_token', token: user.confirmation_token)
    end
  end
end

app/services/users/phone_or_email_updater.rb

# frozen_string_literal: true

module Users
  class PhoneOrEmailUpdater < BaseService
    def call
      authorize!(current_user, to: :user?)

      current_user.tap do |user|
        user.update!(unconfirmed_mobile: params[:unconfirmed_mobile], unconfirmed_email: params[:unconfirmed_email])
        ConfirmationCodeSender.new(id: user.id).call
      end
    end
  end
end

config/nitializers/confirmable.rb

# frozen_string_literal: true

# Overriding this model to support the confirmation for mobile number as well

module Devise
  module Models
    module Confirmable
      def confirm(args = {})
        pending_any_confirmation do
          return expired_error if confirmation_period_expired?

          self.confirmed_at = Time.now.utc
          saved = saved(args)
          after_confirmation if saved
          saved
        end
      end

      def saved(args)
        @saved ||= if pending_reconfirmation?
                     skip_reconfirmation!
                     save!(validate: true)
                   else
                     save!(validate: args[:ensure_valid] == true)
                   end
      end

      def pending_reconfirmation?
        if unconfirmed_email.present?
          self.email = unconfirmed_email
          self.unconfirmed_email = nil
          true
        elsif unconfirmed_mobile.present?
          self.mobile_number = unconfirmed_mobile
          self.unconfirmed_mobile = nil
          true
        else
          false
        end
      end

      private

      def expired_error
        errors.add(
          :email,
          :confirmation_period_expired,
          period: Devise::TimeInflector.time_ago_in_words(self.class.confirm_within.ago)
        )
        false
      end
    end
  end
end

Mobile update seems to be working fine but email is not updating. I am using graphql to update the email

In console I tried using .confirm but it seems to be not working as well the user email is not getting confirmed

JIGME
  • 23
  • 4

1 Answers1

0

In your pending_reconfirmation?, self.unconfirmed_email is assigned to be nil. It seems like pending_reconfirmation? is only called in saved, however, it is called by pending_any_confirmation, too.

https://github.com/heartcombo/devise/blob/8593801130f2df94a50863b5db535c272b00efe1/lib/devise/models/confirmable.rb#L238

# Checks whether the record requires any confirmation.
def pending_any_confirmation
  if (!confirmed? || pending_reconfirmation?)
    yield
  else
    self.errors.add(:email, :already_confirmed)
    false
  end
end

So when the second time the pending_reconfirmation? is called in the saved, pending_reconfirmation? will return false because unconfirmed_email is nil.

You'd better not do actual assignments inside the methods end with ? it will be an implicit side-effect. Maybe create a new method end with ! to change the value of attributes.

For example:

module Devise
  module Models
    module Confirmable
      def confirm(args = {})
        pending_any_confirmation do
          return expired_error if confirmation_period_expired?

          self.confirmed_at = Time.now.utc
          saved = saved(args)
          after_confirmation if saved
          saved
        end
      end

      def saved(args)
        @saved ||= if pending_reconfirmation?
          reconfirm_email! if unconfirmed_email.present?
          reconfirm_mobile! if unconfirmed_mobile.present?
          skip_reconfirmation!
          save!(validate: true)
        else
          save!(validate: args[:ensure_valid] == true)
        end
      end

      def pending_reconfirmation?
        unconfirmed_email.present? || nconfirmed_mobile.present?
      end

      def reconfirm_email!
        self.email = unconfirmed_email
        self.unconfirmed_email = nil
      end

      def reconfirm_mobile!
        self.mobile_number = unconfirmed_mobile
        self.unconfirmed_mobile = nil
      end

      private

      def expired_error
        errors.add(
          :email,
          :confirmation_period_expired,
          period: Devise::TimeInflector.time_ago_in_words(self.class.confirm_within.ago)
        )
        false
      end
    end
  end
end
kevinluo201
  • 1,444
  • 14
  • 18