spree_otp_auth 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +42 -0
  3. data/LICENSE +29 -0
  4. data/README.md +190 -0
  5. data/app/controllers/spree/admin/otp_configurations_controller.rb +50 -0
  6. data/app/controllers/spree/api/v3/store/otp_controller.rb +301 -0
  7. data/app/mailers/spree/otp_mailer.rb +57 -0
  8. data/app/models/spree/otp_attempt.rb +82 -0
  9. data/app/models/spree/otp_configuration.rb +90 -0
  10. data/app/models/spree/otp_store_decorator.rb +15 -0
  11. data/app/models/spree/otp_user_decorator.rb +59 -0
  12. data/app/services/spree/otp/code_sent_payload.rb +55 -0
  13. data/app/services/spree/otp/delivery.rb +77 -0
  14. data/app/services/spree/otp/fallback.rb +86 -0
  15. data/app/services/spree/otp/generate.rb +198 -0
  16. data/app/services/spree/otp/identity.rb +81 -0
  17. data/app/services/spree/otp/marketing_consent.rb +35 -0
  18. data/app/services/spree/otp/rate_limiter.rb +82 -0
  19. data/app/services/spree/otp/resend.rb +27 -0
  20. data/app/services/spree/otp/verify.rb +173 -0
  21. data/app/subscribers/spree/otp/delivery_failed_subscriber.rb +43 -0
  22. data/app/views/spree/admin/otp_configurations/edit.html.erb +141 -0
  23. data/app/views/spree/otp_mailer/send_otp.html.erb +100 -0
  24. data/app/views/spree/otp_mailer/send_otp.text.erb +8 -0
  25. data/config/locales/en.yml +39 -0
  26. data/config/routes.rb +22 -0
  27. data/db/migrate/20260927000001_create_spree_otp_configurations.spree_otp_auth.rb +20 -0
  28. data/db/migrate/20260927000002_create_spree_otp_attempts.spree_otp_auth.rb +26 -0
  29. data/db/migrate/20260927000003_add_otp_fields_to_spree_users.spree_otp_auth.rb +12 -0
  30. data/db/migrate/20260927000004_update_otp_schema_for_mandatory_mobile_and_templates.spree_otp_auth.rb +28 -0
  31. data/db/migrate/20261018000001_add_gupshup_fields_to_spree_otp_configurations.spree_otp_auth.rb +18 -0
  32. data/db/migrate/20261018000002_add_delivery_fields_to_spree_otp_attempts.spree_otp_auth.rb +18 -0
  33. data/db/migrate/20261018000003_add_email_marketing_decided_at_to_spree_users.spree_otp_auth.rb +30 -0
  34. data/db/migrate/20261019000004_add_delivery_message_ref_to_spree_otp_attempts.spree_otp_auth.rb +8 -0
  35. data/lib/generators/spree_otp_auth/install/install_generator.rb +27 -0
  36. data/lib/spree_otp_auth/engine.rb +44 -0
  37. data/lib/spree_otp_auth/version.rb +5 -0
  38. data/lib/spree_otp_auth.rb +28 -0
  39. data/spree_otp_auth.gemspec +39 -0
  40. metadata +167 -0
@@ -0,0 +1,8 @@
1
+ <%= @store.name %> Verification Code
2
+ ========================================
3
+
4
+ Your verification code is: <%= @code %>
5
+
6
+ This code expires in <%= @expiry_minutes %> minutes.
7
+
8
+ If you did not request this login code, please safely disregard this email.
@@ -0,0 +1,39 @@
1
+ en:
2
+ spree:
3
+ admin:
4
+ tab:
5
+ otp_authentication: "OTP Authentication"
6
+ otp_authentication: "OTP Authentication"
7
+ otp_auth:
8
+ admin:
9
+ title: "OTP Authentication Settings"
10
+ update_success: "OTP authentication settings updated successfully."
11
+ channel_header: "Channel & Timing Configuration"
12
+ active_channel: "Active OTP Channel"
13
+ otp_length: "OTP Length"
14
+ otp_expiry_seconds: "OTP Expiry (seconds)"
15
+ resend_cooldown_seconds: "Resend Cooldown (seconds)"
16
+ fallback_delay_seconds: "WhatsApp Fallback Delay (seconds)"
17
+ msg91_header: "MSG91 Provider Credentials & DLT Templates"
18
+ msg91_auth_key: "MSG91 Auth Key"
19
+ msg91_sender_id: "Sender ID (Header)"
20
+ msg91_sms_template_id: "SMS Template ID"
21
+ msg91_whatsapp_template_id: "WhatsApp Template ID"
22
+ gupshup_header: "WhatsApp via Gupshup"
23
+ gupshup_source_number: "Business WhatsApp number"
24
+ gupshup_app_name: "Gupshup app name"
25
+ gupshup_app_id: "Gupshup app ID"
26
+ gupshup_template_name: "Template name"
27
+ gupshup_template_id: "Template ID (optional)"
28
+ gupshup_api_key_status: "API key"
29
+ gupshup_webhook_url: "Delivery callback URL"
30
+ email_backup: "Email backup"
31
+ test_whatsapp: "Send test WhatsApp"
32
+ errors:
33
+ requires_email: "Please enter your email address to continue."
34
+ invalid_email: "Please enter a valid email address."
35
+ email_linked_to_other_mobile: "This email is already linked to another mobile number. Please sign in with the mobile number linked to that email."
36
+ invalid_email_code: "The code sent to your email is incorrect or expired."
37
+ email_fallback_disabled: "Email codes are not available right now."
38
+ no_email_on_account: "There is no email on this account to send a code to."
39
+ whatsapp_not_ready: "WhatsApp can't be switched on yet."
data/config/routes.rb ADDED
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ Spree::Core::Engine.add_routes do
4
+ namespace :api, defaults: { format: 'json' } do
5
+ namespace :v3 do
6
+ namespace :store do
7
+ scope :otp, controller: 'otp' do
8
+ post :request, action: :request_otp
9
+ post :verify, action: :verify
10
+ post :resend, action: :resend
11
+ post :fallback, action: :fallback
12
+ post :status, action: :delivery_status
13
+ patch 'customer/mobile', action: :update_mobile
14
+ end
15
+ end
16
+ end
17
+ end
18
+
19
+ namespace :admin do
20
+ resource :otp_configuration, only: [:edit, :update]
21
+ end
22
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateSpreeOtpConfigurations < ActiveRecord::Migration[8.1]
4
+ def change
5
+ create_table :spree_otp_configurations do |t|
6
+ t.references :store, null: false, foreign_key: { to_table: :spree_stores }, index: { unique: true }
7
+ t.string :active_channel, null: false, default: 'email'
8
+ t.integer :otp_length, null: false, default: 6
9
+ t.integer :otp_expiry_seconds, null: false, default: 600
10
+ t.integer :resend_cooldown_seconds, null: false, default: 30
11
+ t.integer :fallback_delay_seconds, null: false, default: 20
12
+ t.text :msg91_auth_key
13
+ t.string :msg91_sms_template_id
14
+ t.string :msg91_whatsapp_template_id
15
+ t.string :msg91_sender_id
16
+
17
+ t.timestamps
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateSpreeOtpAttempts < ActiveRecord::Migration[8.1]
4
+ def change
5
+ create_table :spree_otp_attempts do |t|
6
+ t.references :store, null: false, foreign_key: { to_table: :spree_stores }, index: true
7
+ t.references :user, foreign_key: { to_table: :spree_users }, index: true
8
+ t.string :identifier, null: false
9
+ t.string :channel, null: false
10
+ t.string :code_digest, null: false
11
+ t.string :salt, null: false
12
+ t.integer :attempts_count, null: false, default: 0
13
+ t.integer :max_attempts, null: false, default: 5
14
+ t.datetime :expires_at, null: false
15
+ t.datetime :verified_at
16
+ t.string :ip_address
17
+ t.string :user_agent
18
+
19
+ t.timestamps
20
+ end
21
+
22
+ add_index :spree_otp_attempts, [:identifier, :expires_at]
23
+ add_index :spree_otp_attempts, :verified_at
24
+ add_index :spree_otp_attempts, :created_at
25
+ end
26
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AddOtpFieldsToSpreeUsers < ActiveRecord::Migration[8.1]
4
+ def change
5
+ add_column :spree_users, :auth_identifier, :string unless column_exists?(:spree_users, :auth_identifier)
6
+ add_column :spree_users, :auth_channel, :string, default: 'email', null: false unless column_exists?(:spree_users, :auth_channel)
7
+ add_column :spree_users, :mobile_number, :string unless column_exists?(:spree_users, :mobile_number)
8
+
9
+ add_index :spree_users, :auth_identifier unless index_exists?(:spree_users, :auth_identifier)
10
+ add_index :spree_users, :mobile_number, unique: true, where: "mobile_number IS NOT NULL AND mobile_number != ''", name: 'idx_spree_users_on_unique_mobile_number' unless index_exists?(:spree_users, :mobile_number, name: 'idx_spree_users_on_unique_mobile_number')
11
+ end
12
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ class UpdateOtpSchemaForMandatoryMobileAndTemplates < ActiveRecord::Migration[8.1]
4
+ def change
5
+ # 1. spree_otp_configurations: fallback channel and email template association
6
+ unless column_exists?(:spree_otp_configurations, :fallback_channel)
7
+ add_column :spree_otp_configurations, :fallback_channel, :string, default: 'none', null: false
8
+ end
9
+
10
+ unless column_exists?(:spree_otp_configurations, :email_template_id)
11
+ if table_exists?(:spree_email_templates)
12
+ add_reference :spree_otp_configurations, :email_template, foreign_key: { to_table: :spree_email_templates }, index: true
13
+ else
14
+ add_reference :spree_otp_configurations, :email_template, index: true
15
+ end
16
+ end
17
+
18
+ # 2. spree_otp_attempts: staged mobile number for progressive disclosure flow
19
+ unless column_exists?(:spree_otp_attempts, :staged_mobile_number)
20
+ add_column :spree_otp_attempts, :staged_mobile_number, :string
21
+ end
22
+
23
+ # 3. spree_users: confirmed at timestamp for mobile phone verification
24
+ unless column_exists?(:spree_users, :mobile_confirmed_at)
25
+ add_column :spree_users, :mobile_confirmed_at, :datetime
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AddGupshupFieldsToSpreeOtpConfigurations < ActiveRecord::Migration[8.1]
4
+ def change
5
+ add_column :spree_otp_configurations, :gupshup_source_number, :string unless column_exists?(:spree_otp_configurations, :gupshup_source_number)
6
+ add_column :spree_otp_configurations, :gupshup_app_name, :string unless column_exists?(:spree_otp_configurations, :gupshup_app_name)
7
+ add_column :spree_otp_configurations, :gupshup_app_id, :string unless column_exists?(:spree_otp_configurations, :gupshup_app_id)
8
+ unless column_exists?(:spree_otp_configurations, :gupshup_template_name)
9
+ add_column :spree_otp_configurations, :gupshup_template_name, :string, default: 'verify_otp'
10
+ end
11
+ add_column :spree_otp_configurations, :gupshup_template_id, :string unless column_exists?(:spree_otp_configurations, :gupshup_template_id)
12
+
13
+ # New stores default to WhatsApp with email backup. Existing rows keep their
14
+ # channel until an admin switches after a successful test send.
15
+ change_column_default :spree_otp_configurations, :active_channel, from: 'email', to: 'whatsapp'
16
+ change_column_default :spree_otp_configurations, :fallback_channel, from: 'none', to: 'email'
17
+ end
18
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AddDeliveryFieldsToSpreeOtpAttempts < ActiveRecord::Migration[8.1]
4
+ def change
5
+ change_table :spree_otp_attempts, bulk: true do |t|
6
+ t.string :purpose, null: false, default: 'login' unless column_exists?(:spree_otp_attempts, :purpose)
7
+ t.string :staged_email unless column_exists?(:spree_otp_attempts, :staged_email)
8
+ t.string :provider_message_id unless column_exists?(:spree_otp_attempts, :provider_message_id)
9
+ t.string :delivery_status, null: false, default: 'pending' unless column_exists?(:spree_otp_attempts, :delivery_status)
10
+ t.string :fallback_reason unless column_exists?(:spree_otp_attempts, :fallback_reason)
11
+ end
12
+
13
+ unless column_exists?(:spree_otp_attempts, :parent_attempt_id)
14
+ add_reference :spree_otp_attempts, :parent_attempt, foreign_key: { to_table: :spree_otp_attempts }, index: true
15
+ end
16
+ add_index :spree_otp_attempts, :provider_message_id unless index_exists?(:spree_otp_attempts, :provider_message_id)
17
+ end
18
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AddEmailMarketingDecidedAtToSpreeUsers < ActiveRecord::Migration[8.1]
4
+ # Null = the customer never made a news-and-offers choice; with
5
+ # accepts_email_marketing = false and a timestamp, they opted out.
6
+ def up
7
+ add_column :spree_users, :email_marketing_decided_at, :datetime unless column_exists?(:spree_users, :email_marketing_decided_at)
8
+
9
+ # Customers already on a verified newsletter subscription count as
10
+ # subscribed, decided when they first subscribed.
11
+ execute <<~SQL.squish
12
+ UPDATE spree_users u
13
+ SET accepts_email_marketing = TRUE,
14
+ email_marketing_decided_at = s.first_subscribed_at
15
+ FROM (
16
+ SELECT COALESCE(ns.user_id, matched.id) AS user_id, MIN(ns.created_at) AS first_subscribed_at
17
+ FROM spree_newsletter_subscribers ns
18
+ LEFT JOIN spree_users matched
19
+ ON ns.user_id IS NULL AND LOWER(matched.email) = LOWER(ns.email)
20
+ WHERE ns.verified_at IS NOT NULL
21
+ GROUP BY COALESCE(ns.user_id, matched.id)
22
+ ) s
23
+ WHERE s.user_id IS NOT NULL AND u.id = s.user_id
24
+ SQL
25
+ end
26
+
27
+ def down
28
+ remove_column :spree_users, :email_marketing_decided_at if column_exists?(:spree_users, :email_marketing_decided_at)
29
+ end
30
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AddDeliveryMessageRefToSpreeOtpAttempts < ActiveRecord::Migration[8.1]
4
+ def change
5
+ add_column :spree_otp_attempts, :delivery_message_ref, :uuid unless column_exists?(:spree_otp_attempts, :delivery_message_ref)
6
+ add_index :spree_otp_attempts, :delivery_message_ref unless index_exists?(:spree_otp_attempts, :delivery_message_ref)
7
+ end
8
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators'
4
+
5
+ module SpreeOtpAuth
6
+ module Generators
7
+ class InstallGenerator < Rails::Generators::Base
8
+ desc 'Install spree_otp_auth migrations and assets'
9
+
10
+ class_option :migrate, type: :boolean, default: true, banner: 'Run migrations'
11
+
12
+ # spree_otp_auth sends codes through spree_notification_providers, so its
13
+ # tables are installed too (already-copied migrations are skipped).
14
+ def add_migrations
15
+ run 'bundle exec rake railties:install:migrations FROM=spree_notification_providers,spree_otp_auth'
16
+ end
17
+
18
+ def run_migrations
19
+ if options[:migrate]
20
+ run 'bundle exec rake db:migrate'
21
+ else
22
+ say_status :skipping, 'db:migrate, remember to run migrations manually'
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SpreeOtpAuth
4
+ class Engine < ::Rails::Engine
5
+ engine_name 'spree_otp_auth'
6
+
7
+ config.generators do |g|
8
+ g.test_framework :rspec
9
+ end
10
+
11
+ # The verify endpoint takes the one-time code as `code`; keep it out of the
12
+ # "Parameters:" log lines. Exact match so promo_code, country_code, etc.
13
+ # stay readable.
14
+ initializer 'spree_otp_auth.filter_parameters' do |app|
15
+ app.config.filter_parameters += [/\Acode\z/]
16
+ end
17
+
18
+ # OTP codes go out through spree_notification_providers; the code's
19
+ # provider-side template is entered per provider under "Login code".
20
+ initializer 'spree_otp_auth.message_types' do
21
+ SpreeNotificationProviders.register_message_type :otp_code,
22
+ label: 'Login code', channels: %i[email sms whatsapp]
23
+ end
24
+
25
+ # A provider that reports a code as failed after accepting it (e.g. a
26
+ # Gupshup "failed" webhook) triggers the email backup. Messages sent before
27
+ # spree_notification_providers have no delivery row, so the webhook asks
28
+ # this resolver which store owns the message id.
29
+ config.after_initialize do
30
+ Spree.subscribers << Spree::Otp::DeliveryFailedSubscriber
31
+ SpreeNotificationProviders.register_message_resolver(:gupshup) do |provider_message_id|
32
+ Spree::OtpAttempt.find_by(provider_message_id: provider_message_id, channel: 'whatsapp')&.store
33
+ end
34
+ end
35
+
36
+ def self.activate
37
+ Dir.glob(File.join(File.dirname(__FILE__), '../../app/**/*_decorator*.rb')) do |c|
38
+ Rails.configuration.cache_classes ? require(c) : load(c)
39
+ end
40
+ end
41
+
42
+ config.to_prepare(&method(:activate).to_proc)
43
+ end
44
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SpreeOtpAuth
4
+ VERSION = '0.2.0'
5
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spree/core'
4
+ require 'spree_notification_providers'
5
+ require 'spree_otp_auth/version'
6
+ require 'spree_otp_auth/engine'
7
+
8
+ module SpreeOtpAuth
9
+ # Domain of the placeholder email given to accounts created from a mobile
10
+ # number alone ("<digits>@guest.example.invalid"). Such emails count as
11
+ # "no email". Set it in an initializer to match your storefront:
12
+ # SpreeOtpAuth.synthetic_email_domain = '@guest.example.local'
13
+ mattr_accessor :synthetic_email_domain, default: '@guest.example.invalid'
14
+
15
+ # Optional model (class name) for admin-editable OTP email templates. Its
16
+ # records need `name`, `render_subject(vars)` and `render_body(vars)`, and
17
+ # may be scoped by a `store_id` column. Unset: the built-in mailer view is used.
18
+ # SpreeOtpAuth.email_template_class = 'Spree::EmailTemplate'
19
+ mattr_accessor :email_template_class, default: nil
20
+
21
+ def self.email_template_model
22
+ email_template_class.presence&.safe_constantize
23
+ end
24
+
25
+ class Error < StandardError; end
26
+ class DeliveryError < Error; end
27
+ class RateLimitError < Error; end
28
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/spree_otp_auth/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'spree_otp_auth'
7
+ spec.version = SpreeOtpAuth::VERSION
8
+ spec.authors = ['ONTODI']
9
+ spec.summary = 'Passwordless multi-channel OTP authentication and login-gated checkout for Spree Commerce'
10
+ spec.description = <<~DESC.strip
11
+ Provides mandatory login-gated checkout, passwordless customer authentication over WhatsApp,
12
+ SMS or email with an automatic email backup, and store-scoped admin configuration. Codes are
13
+ delivered through spree_notification_providers, so any provider and priority order works.
14
+ DESC
15
+ spec.license = 'BSD-3-Clause'
16
+
17
+ spec.required_ruby_version = '>= 3.3'
18
+
19
+ spec.files = Dir[
20
+ '{app,config,db,lib}/**/*',
21
+ 'README.md',
22
+ 'CHANGELOG.md',
23
+ 'LICENSE',
24
+ 'spree_otp_auth.gemspec'
25
+ ]
26
+ spec.require_paths = ['lib']
27
+
28
+ spec.add_dependency 'rails', '>= 8.0'
29
+ spec.add_dependency 'spree', '>= 5.5'
30
+ spec.add_dependency 'spree_core', '>= 5.5'
31
+ spec.add_dependency 'spree_api', '>= 5.5'
32
+ spec.add_dependency 'spree_notification_providers', '~> 0.1'
33
+ spec.add_dependency 'spree_admin', '>= 5.5'
34
+
35
+ spec.homepage = 'https://github.com/kalanjiyaVishnu/spree_otp_auth'
36
+ spec.metadata['source_code_uri'] = spec.homepage
37
+ spec.metadata['changelog_uri'] = "#{spec.homepage}/blob/main/CHANGELOG.md"
38
+ spec.metadata['rubygems_mfa_required'] = 'true'
39
+ end
metadata ADDED
@@ -0,0 +1,167 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: spree_otp_auth
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
+ platform: ruby
6
+ authors:
7
+ - ONTODI
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: rails
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '8.0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '8.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: spree
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '5.5'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '5.5'
40
+ - !ruby/object:Gem::Dependency
41
+ name: spree_core
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '5.5'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '5.5'
54
+ - !ruby/object:Gem::Dependency
55
+ name: spree_api
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '5.5'
61
+ type: :runtime
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '5.5'
68
+ - !ruby/object:Gem::Dependency
69
+ name: spree_notification_providers
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '0.1'
75
+ type: :runtime
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '0.1'
82
+ - !ruby/object:Gem::Dependency
83
+ name: spree_admin
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: '5.5'
89
+ type: :runtime
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ version: '5.5'
96
+ description: |-
97
+ Provides mandatory login-gated checkout, passwordless customer authentication over WhatsApp,
98
+ SMS or email with an automatic email backup, and store-scoped admin configuration. Codes are
99
+ delivered through spree_notification_providers, so any provider and priority order works.
100
+ executables: []
101
+ extensions: []
102
+ extra_rdoc_files: []
103
+ files:
104
+ - CHANGELOG.md
105
+ - LICENSE
106
+ - README.md
107
+ - app/controllers/spree/admin/otp_configurations_controller.rb
108
+ - app/controllers/spree/api/v3/store/otp_controller.rb
109
+ - app/mailers/spree/otp_mailer.rb
110
+ - app/models/spree/otp_attempt.rb
111
+ - app/models/spree/otp_configuration.rb
112
+ - app/models/spree/otp_store_decorator.rb
113
+ - app/models/spree/otp_user_decorator.rb
114
+ - app/services/spree/otp/code_sent_payload.rb
115
+ - app/services/spree/otp/delivery.rb
116
+ - app/services/spree/otp/fallback.rb
117
+ - app/services/spree/otp/generate.rb
118
+ - app/services/spree/otp/identity.rb
119
+ - app/services/spree/otp/marketing_consent.rb
120
+ - app/services/spree/otp/rate_limiter.rb
121
+ - app/services/spree/otp/resend.rb
122
+ - app/services/spree/otp/verify.rb
123
+ - app/subscribers/spree/otp/delivery_failed_subscriber.rb
124
+ - app/views/spree/admin/otp_configurations/edit.html.erb
125
+ - app/views/spree/otp_mailer/send_otp.html.erb
126
+ - app/views/spree/otp_mailer/send_otp.text.erb
127
+ - config/locales/en.yml
128
+ - config/routes.rb
129
+ - db/migrate/20260927000001_create_spree_otp_configurations.spree_otp_auth.rb
130
+ - db/migrate/20260927000002_create_spree_otp_attempts.spree_otp_auth.rb
131
+ - db/migrate/20260927000003_add_otp_fields_to_spree_users.spree_otp_auth.rb
132
+ - db/migrate/20260927000004_update_otp_schema_for_mandatory_mobile_and_templates.spree_otp_auth.rb
133
+ - db/migrate/20261018000001_add_gupshup_fields_to_spree_otp_configurations.spree_otp_auth.rb
134
+ - db/migrate/20261018000002_add_delivery_fields_to_spree_otp_attempts.spree_otp_auth.rb
135
+ - db/migrate/20261018000003_add_email_marketing_decided_at_to_spree_users.spree_otp_auth.rb
136
+ - db/migrate/20261019000004_add_delivery_message_ref_to_spree_otp_attempts.spree_otp_auth.rb
137
+ - lib/generators/spree_otp_auth/install/install_generator.rb
138
+ - lib/spree_otp_auth.rb
139
+ - lib/spree_otp_auth/engine.rb
140
+ - lib/spree_otp_auth/version.rb
141
+ - spree_otp_auth.gemspec
142
+ homepage: https://github.com/kalanjiyaVishnu/spree_otp_auth
143
+ licenses:
144
+ - BSD-3-Clause
145
+ metadata:
146
+ source_code_uri: https://github.com/kalanjiyaVishnu/spree_otp_auth
147
+ changelog_uri: https://github.com/kalanjiyaVishnu/spree_otp_auth/blob/main/CHANGELOG.md
148
+ rubygems_mfa_required: 'true'
149
+ rdoc_options: []
150
+ require_paths:
151
+ - lib
152
+ required_ruby_version: !ruby/object:Gem::Requirement
153
+ requirements:
154
+ - - ">="
155
+ - !ruby/object:Gem::Version
156
+ version: '3.3'
157
+ required_rubygems_version: !ruby/object:Gem::Requirement
158
+ requirements:
159
+ - - ">="
160
+ - !ruby/object:Gem::Version
161
+ version: '0'
162
+ requirements: []
163
+ rubygems_version: 4.0.3
164
+ specification_version: 4
165
+ summary: Passwordless multi-channel OTP authentication and login-gated checkout for
166
+ Spree Commerce
167
+ test_files: []