registration.rb

   1# frozen_string_literal: true
   2
   3require "erb"
   4require "ruby-bandwidth-iris"
   5require "securerandom"
   6
   7require_relative "./alt_top_up_form"
   8require_relative "./bandwidth_tn_order"
   9require_relative "./bandwidth_tn_reservation_repo"
  10require_relative "./command"
  11require_relative "./em"
  12require_relative "./invites_repo"
  13require_relative "./oob"
  14require_relative "./parent_code_repo"
  15require_relative "./proxied_jid"
  16require_relative "./tel_selections"
  17require_relative "./welcome_message"
  18require_relative "./sim_kind"
  19require_relative "./onboarding"
  20
  21def reload_customer(customer)
  22	EMPromise.resolve(nil).then do
  23		Command.execution.customer_repo.find(customer.customer_id)
  24	end
  25end
  26
  27def handle_prev(customer, googleplay_user_id, product)
  28	if product.is_a?(SIMKind)
  29		Registration::DataOnly.for(customer, product)
  30	else
  31		Registration::Activation.for(customer, googleplay_user_id, product)
  32	end
  33end
  34
  35class Registration
  36	class PayForSim
  37		def initialize(customer, sim_kind)
  38			@customer = customer
  39			@sim_kind = sim_kind
  40		end
  41
  42		def write
  43			Command.reply { |reply|
  44				reply.command << FormTemplate.render(
  45					"registration/pay_without_code",
  46					product: @sim_kind,
  47					instructions: instructions,
  48					currency_required: !@customer.currency,
  49					title: "Pay for #{@sim_kind.klass.label}"
  50				)
  51			}.then(&method(:parse)).then(&:write)
  52		end
  53
  54		def instructions
  55			cfg = @sim_kind.cfg(@customer.currency)
  56			return nil unless cfg
  57
  58			<<~I
  59				To activate your data SIM, you need to deposit $#{'%.2f' % (cfg[:price] / 100.0)} to your balance.
  60				(If you'd like to pay in another cryptocurrency, currently we recommend using a service like simpleswap.io, morphtoken.com, changenow.io, or godex.io.)
  61			I
  62		end
  63
  64		def parse(iq)
  65			unless @customer.currency
  66				plan = Plan.for_registration(iq.form.field("plan_name").value.to_s)
  67				(@customer = @customer.with_plan(plan.name)).save_plan!
  68			end.then { process_payment(iq) }
  69		end
  70
  71		def process_payment(iq)
  72			Payment.for(
  73				iq, @customer, @sim_kind,
  74				price: @sim_kind.cfg(@customer.currency)[:price],
  75				maybe_bill: Registration::Payment::JustCharge
  76			).write.then { reload_customer(@customer) }.then { |customer|
  77				DataOnly.for(customer, @sim_kind)
  78			}
  79		end
  80	end
  81
  82	class DataOnly
  83		def self.for(customer, sim_kind)
  84			cfg = sim_kind.cfg(customer.currency)
  85			unless cfg && customer.balance > cfg[:price]
  86				return PayForSim.new(customer, sim_kind)
  87			end
  88
  89			new(customer, sim_kind)
  90		end
  91
  92		def initialize(customer, sim_kind)
  93			@customer = customer
  94			@sim_kind = sim_kind
  95		end
  96
  97		def write
  98			@sim_kind.klass.for(
  99				@customer,
 100				**@sim_kind.cfg(@customer.currency)
 101			).then { |order|
 102				# NOTE: cheogram will swallow any stanza
 103				# with type `completed`
 104				# `can_complete: false` is needed to prevent customer
 105				# from (possibly) permanently losing their eSIM QR code
 106				order.process(can_complete: false)
 107			}
 108		end
 109	end
 110
 111	class RegistrationType
 112		def self.for(customer, google_play_userid, product)
 113			if product.is_a?(SIMKind)
 114				return Registration::DataOnly.for(
 115					customer, product
 116				)
 117			end
 118
 119			Registration::FinishOrStartActivation.for(
 120				customer, google_play_userid, product
 121			)
 122		end
 123	end
 124
 125	def self.for(customer, google_play_userid, tel_selections)
 126		if (reg = customer.registered?)
 127			Registered.for(customer, reg.phone)
 128		else
 129			tel_selections[customer.jid].then(&:choose_tel_or_data).then do |product|
 130				reserve_and_continue(tel_selections, customer, product).then do
 131					RegistrationType.for(customer, google_play_userid, product)
 132				end
 133			end
 134		end
 135	end
 136
 137	def self.reserve_and_continue(tel_selections, customer, tel)
 138		tel.reserve(customer).catch do
 139			tel_selections.delete(customer.jid).then {
 140				tel_selections[customer.jid]
 141			}.then { |choose|
 142				choose.choose_tel_or_data(
 143					error: "The JMP number #{tel} is no longer available."
 144				)
 145			}.then { |n_tel| reserve_and_continue(tel_selections, customer, n_tel) }
 146		end
 147	end
 148
 149	class Registered
 150		def self.for(customer, tel)
 151			if customer.jid.onboarding?
 152				FinishOnboarding.for(customer, tel)
 153			else
 154				new(tel)
 155			end
 156		end
 157
 158		def initialize(tel)
 159			@tel = tel
 160		end
 161
 162		def write
 163			Command.finish("You are already registered with JMP number #{@tel}")
 164		end
 165	end
 166
 167	class FinishOrStartActivation
 168		def self.for(customer, google_play_userid, tel)
 169			if customer.active?
 170				Finish.new(customer, tel)
 171			elsif customer.balance >= CONFIG[:activation_amount_accept]
 172				BillPlan.new(customer, tel)
 173			else
 174				new(customer, google_play_userid, tel)
 175			end
 176		end
 177
 178		def initialize(customer, google_play_userid, tel)
 179			@customer = customer
 180			@tel = tel
 181			@google_play_userid = google_play_userid
 182		end
 183
 184		def write
 185			Command.reply { |reply|
 186				reply.allowed_actions = [:next]
 187				reply.note_type = :info
 188				reply.note_text = File.read("#{__dir__}/../fup.txt")
 189			}.then { Activation.for(@customer, @google_play_userid, @tel).write }
 190		end
 191	end
 192
 193	class Activation
 194		def self.for(customer, google_play_userid, tel)
 195			jid = ProxiedJID.new(customer.jid).unproxied
 196			if CONFIG[:approved_domains].key?(jid.domain.to_sym)
 197				Allow.for(customer, tel, jid)
 198			elsif google_play_userid
 199				GooglePlay.new(customer, google_play_userid, tel)
 200			else
 201				new(customer, tel)
 202			end
 203		end
 204
 205		def initialize(customer, tel)
 206			@customer = customer
 207			@tel = tel
 208		end
 209
 210		attr_reader :customer, :tel
 211
 212		def form
 213			FormTemplate.render("registration/activate", tel: tel)
 214		end
 215
 216		def write
 217			Command.reply { |reply|
 218				reply.allowed_actions = [:next]
 219				reply.command << form
 220			}.then(&method(:next_step))
 221		end
 222
 223		def next_step(iq)
 224			EMPromise.resolve(nil).then do
 225				plan = Plan.for_registration(iq.form.field("plan_name").value.to_s)
 226				@customer = @customer.with_plan(plan.name)
 227				Registration::Payment::InviteCode.new(
 228					@customer, @tel, finish: Finish, db: DB, redis: REDIS
 229				).parse(iq, force_save_plan: true)
 230					.catch_only(InvitesRepo::Invalid) do
 231						Payment.for(iq, @customer, @tel).then(&:write)
 232					end
 233			end
 234		end
 235
 236		class GooglePlay
 237			def initialize(customer, google_play_userid, tel)
 238				@customer = customer
 239				@google_play_userid = google_play_userid
 240				@tel = tel
 241				@invites = InvitesRepo.new(DB, REDIS)
 242				@parent_code_repo = ParentCodeRepo.new(redis: REDIS, db: DB)
 243			end
 244
 245			def used
 246				REDIS.sismember("google_play_userids", @google_play_userid)
 247			end
 248
 249			def form
 250				FormTemplate.render(
 251					"registration/google_play",
 252					tel: @tel
 253				)
 254			end
 255
 256			def write
 257				used.then do |u|
 258					next Activation.for(@customer, nil, @tel).write if u.to_s == "1"
 259
 260					Command.reply { |reply|
 261						reply.allowed_actions = [:next]
 262						reply.command << form
 263					}.then(&method(:activate)).then do
 264						Finish.new(@customer, @tel).write
 265					end
 266				end
 267			end
 268
 269			def activate(iq)
 270				plan = Plan.for_registration(iq.form.field("plan_name").value)
 271				code = iq.form.field("code")&.value
 272				EMPromise.all([
 273					@parent_code_repo.find(code),
 274					REDIS.sadd("google_play_userids", @google_play_userid)
 275				]).then { |(parent, _)|
 276					save_active_plan(plan, parent)
 277				}.then do
 278					use_referral_code(code)
 279				end
 280			end
 281
 282		protected
 283
 284			def save_bogus_transaction
 285				Transaction.new(
 286					customer_id: @customer.customer_id,
 287					transaction_id: "google_play_#{@customer.customer_id}",
 288					amount: 0,
 289					note: "Activated via Google Play",
 290					bonus_eligible?: false
 291				).insert
 292			end
 293
 294			def save_active_plan(plan, parent)
 295				@customer = @customer.with_plan(plan.name, parent_customer_id: parent)
 296				save_bogus_transaction.then do
 297					@customer.activate_plan_starting_now
 298				end
 299			end
 300
 301			def use_referral_code(code)
 302				EMPromise.resolve(nil).then {
 303					@invites.claim_code(@customer.customer_id, code) {
 304						@customer.extend_plan
 305					}
 306				}.catch_only(InvitesRepo::Invalid) do
 307					@invites.stash_code(@customer.customer_id, code)
 308				end
 309			end
 310		end
 311
 312		class Allow < Activation
 313			def self.for(customer, tel, jid)
 314				credit_to = CONFIG[:approved_domains][jid.domain.to_sym]
 315				new(customer, tel, credit_to)
 316			end
 317
 318			def initialize(customer, tel, credit_to)
 319				super(customer, tel)
 320				@credit_to = credit_to
 321			end
 322
 323			def form
 324				FormTemplate.render(
 325					"registration/allow",
 326					tel: tel,
 327					domain: customer.jid.domain
 328				)
 329			end
 330
 331			def next_step(iq)
 332				plan = Plan.for_registration(iq.form.field("plan_name").value.to_s)
 333				@customer = customer.with_plan(plan.name)
 334				EMPromise.resolve(nil).then { activate }.then do
 335					Finish.new(customer, tel).write
 336				end
 337			end
 338
 339		protected
 340
 341			def activate
 342				DB.transaction do
 343					if @credit_to
 344						InvitesRepo.new(DB, REDIS).create_claimed_code(
 345							@credit_to,
 346							customer.customer_id
 347						)
 348					end
 349					@customer.activate_plan_starting_now
 350				end
 351			end
 352		end
 353	end
 354
 355	module Payment
 356		def self.kinds
 357			@kinds ||= {}
 358		end
 359
 360		def self.for(
 361			iq, customer, product,
 362			finish: Finish, maybe_bill: MaybeBill,
 363			price: CONFIG[:activation_amount] + product.price
 364		)
 365			kinds.fetch(iq.form.field("activation_method")&.value&.to_s&.to_sym) {
 366				raise "Invalid activation method"
 367			}.call(
 368				customer, product, finish: finish, maybe_bill: maybe_bill, price: price
 369			)
 370		end
 371
 372		class CryptoPaymentMethod
 373			def crypto_addrs
 374				raise NotImplementedError, "Subclass must implement"
 375			end
 376
 377			def reg_form_name
 378				raise NotImplementedError, "Subclass must implement"
 379			end
 380
 381			def sell_prices
 382				raise NotImplementedError, "Subclass must implement"
 383			end
 384
 385			def initialize(
 386				customer, product,
 387				price: CONFIG[:activation_amount] + product.price, **
 388			)
 389				@customer = customer
 390				@customer_id = customer.customer_id
 391				@product = product
 392				@price = price
 393			end
 394
 395			def save
 396				return if product.is_a?(SIMKind)
 397
 398				TEL_SELECTIONS.set(@customer.jid, @product)
 399			end
 400
 401			attr_reader :customer_id, :product
 402
 403			def form(rate, addr)
 404				FormTemplate.render(
 405					reg_form_name,
 406					amount: @price / rate,
 407					addr: addr
 408				)
 409			end
 410
 411			def write
 412				EMPromise.all([addr_and_rate, save]).then do |((addr, rate), _)|
 413					Command.reply { |reply|
 414						reply.allowed_actions = [:prev]
 415						reply.status = :canceled
 416						reply.command << form(rate, addr)
 417					}.then(&method(:handle_possible_prev))
 418				end
 419			end
 420
 421		protected
 422
 423			def handle_possible_prev(iq)
 424				raise "Action not allowed" unless iq.prev?
 425
 426				handle_prev(@customer, nil, @product)
 427			end
 428
 429			def addr_and_rate
 430				EMPromise.all([
 431					crypto_addrs.then { |addrs|
 432						addrs.first || add_crypto_addr
 433					},
 434
 435					sell_prices.public_send(@customer.currency.to_s.downcase)
 436				])
 437			end
 438		end
 439
 440		class Bitcoin < CryptoPaymentMethod
 441			Payment.kinds[:bitcoin] = method(:new)
 442
 443			def reg_form_name
 444				"registration/btc"
 445			end
 446
 447			def sell_prices
 448				BTC_SELL_PRICES
 449			end
 450
 451			def crypto_addrs
 452				@customer.btc_addresses
 453			end
 454
 455			def add_crypto_addr
 456				@customer.add_btc_address
 457			end
 458		end
 459
 460		## Like Bitcoin
 461		class BCH < CryptoPaymentMethod
 462			Payment.kinds[:bch] = method(:new)
 463
 464			def reg_form_name
 465				"registration/bch"
 466			end
 467
 468			def sell_prices
 469				BCH_SELL_PRICES
 470			end
 471
 472			def crypto_addrs
 473				@customer.bch_addresses
 474			end
 475
 476			def add_crypto_addr
 477				@customer.add_bch_address
 478			end
 479		end
 480
 481		class MaybeBill
 482			def initialize(customer, tel, finish: Finish)
 483				@customer = customer
 484				@tel = tel
 485				@finish = finish
 486			end
 487
 488			def call
 489				reload_customer(@customer).then do |customer|
 490					if customer.balance >= CONFIG[:activation_amount_accept]
 491						next BillPlan.new(customer, @tel, finish: @finish)
 492					end
 493
 494					yield customer
 495				end
 496			end
 497		end
 498
 499		class JustCharge
 500			def initialize(customer, *, **)
 501				@customer = customer
 502			end
 503
 504			def call; end
 505		end
 506
 507		class CreditCard
 508			Payment.kinds[:credit_card] = method(:new)
 509
 510			def initialize(
 511				customer, product,
 512				finish: Finish, maybe_bill: MaybeBill,
 513				price: CONFIG[:activation_amount] + product.price
 514			)
 515				@customer = customer
 516				@product = product
 517				@finish = finish
 518				@maybe_bill = maybe_bill.new(customer, product, finish: finish)
 519				@price = price
 520			end
 521
 522			def oob(reply)
 523				oob = OOB.find_or_create(reply.command)
 524				oob.url = CONFIG[:credit_card_url].call(
 525					reply.to.stripped.to_s.gsub("\\", "%5C"),
 526					@customer.customer_id
 527				) + "&amount=#{@price.ceil}"
 528				oob.desc = "Pay by credit card, save, then next here to continue"
 529				oob
 530			end
 531
 532			def write
 533				Command.reply { |reply|
 534					reply.allowed_actions = [:next, :prev]
 535					toob = oob(reply)
 536					reply.note_type = :info
 537					reply.note_text = "#{toob.desc}: #{toob.url}"
 538				}.then do |iq|
 539					handle_prev(@customer, nil, @product) if iq.prev?
 540
 541					@maybe_bill.call { self }&.then(&:write)
 542				end
 543			end
 544		end
 545
 546		class InviteCode
 547			Payment.kinds[:code] = method(:new)
 548
 549			FIELDS = [{
 550				var: "code",
 551				type: "text-single",
 552				label: "Your referral code",
 553				required: true
 554			}].freeze
 555
 556			def initialize(
 557				customer, tel,
 558				error: nil, finish: Finish, db: DB, redis: REDIS, **
 559			)
 560				@customer = customer
 561				@tel = tel
 562				@error = error
 563				@finish = finish
 564				@invites_repo = InvitesRepo.new(db, redis)
 565				@parent_code_repo = ParentCodeRepo.new(db: db, redis: redis)
 566			end
 567
 568			def add_form(reply)
 569				form = reply.form
 570				form.type = :form
 571				form.title = "Enter Referral Code"
 572				form.instructions = @error if @error
 573				form.fields = FIELDS
 574			end
 575
 576			def write
 577				Command.reply { |reply|
 578					reply.allowed_actions = [:next, :prev]
 579					add_form(reply)
 580				}.then(&method(:parse)).catch_only(InvitesRepo::Invalid) { |e|
 581					invalid_code(e).write
 582				}
 583			end
 584
 585			def parse(iq, force_save_plan: false)
 586				return Activation.for(@customer, nil, @tel).then(&:write) if iq.prev?
 587
 588				verify(iq.form.field("code")&.value&.to_s, force_save_plan)
 589					.then(&:write)
 590			end
 591
 592		protected
 593
 594			def invalid_code(e)
 595				self.class.new(@customer, @tel, error: e.message, finish: @finish)
 596			end
 597
 598			def customer_id
 599				@customer.customer_id
 600			end
 601
 602			def verify(code, force_save_plan)
 603				@parent_code_repo.claim_code(@customer, code) {
 604					check_parent_balance
 605				}.catch_only(ParentCodeRepo::Invalid) {
 606					(@customer.save_plan! if force_save_plan).then do
 607						@invites_repo.claim_code(customer_id, code) {
 608							@customer.activate_plan_starting_now
 609						}.then { @finish.new(@customer, @tel) }
 610					end
 611				}
 612			end
 613
 614			def check_parent_balance
 615				MaybeBill.new(@customer, @tel, finish: @finish).call do
 616					msg = "Account balance not enough to cover the activation"
 617					invalid_code(RuntimeError.new(msg))
 618				end
 619			end
 620		end
 621
 622		class Mail
 623			Payment.kinds[:mail] = method(:new)
 624
 625			def initialize(
 626				customer, tel,
 627				price: CONFIG[:activation_amount] + tel.price, **
 628			)
 629				@customer = customer
 630				@tel = tel
 631				@price = price
 632			end
 633
 634			def form
 635				FormTemplate.render(
 636					"registration/mail",
 637					currency: @customer.currency,
 638					price: @price,
 639					**onboarding_extras
 640				)
 641			end
 642
 643			def onboarding_extras
 644				jid = ProxiedJID.new(@customer.jid).unproxied
 645				return {} unless jid.domain == CONFIG[:onboarding_domain]
 646
 647				{
 648					customer_id: @customer.customer_id,
 649					in_note: "Customer ID"
 650				}
 651			end
 652
 653			def write
 654				Command.reply { |reply|
 655					reply.allowed_actions = [:prev]
 656					reply.status = :canceled
 657					reply.command << form
 658				}.then { |iq|
 659					raise "Action not allowed" unless iq.prev?
 660
 661					Activation.for(@customer, nil, @tel).then(&:write)
 662				}
 663			end
 664		end
 665	end
 666
 667	class BillPlan
 668		def initialize(customer, tel, finish: Finish)
 669			@customer = customer
 670			@tel = tel
 671			@finish = finish
 672		end
 673
 674		def write
 675			@customer.bill_plan(note: "Bill #{@tel} for first month").then do
 676				updated_customer =
 677					@customer.with_balance(@customer.balance - @customer.monthly_price)
 678				@finish.new(updated_customer, @tel).write
 679			end
 680		end
 681	end
 682
 683	class BuyNumber
 684		def initialize(customer, tel)
 685			@customer = customer
 686			@tel = tel
 687		end
 688
 689		def instructions
 690			<<~I
 691				You've selected #{@tel} as your JMP number.
 692				To activate your account, you can either deposit $#{'%.2f' % (CONFIG[:activation_amount] + @tel.price)} to your balance or enter your referral code if you have one.
 693				(If you'd like to pay in another cryptocurrency, currently we recommend using a service like simpleswap.io, morphtoken.com, changenow.io, or godex.io.)
 694			I
 695		end
 696
 697		def write
 698			Command.reply { |reply|
 699				reply.command << FormTemplate.render(
 700					"registration/pay_without_code",
 701					product: @tel,
 702					instructions: instructions,
 703					title: "Purchase Number"
 704				)
 705			}.then(&method(:parse)).then(&:write)
 706		end
 707
 708	protected
 709
 710		def parse(iq)
 711			Payment.for(
 712				iq, @customer, @tel,
 713				maybe_bill: ::Registration::Payment::JustCharge,
 714				price: @tel.price
 715			)
 716		end
 717	end
 718
 719	class Finish
 720		def initialize(customer, tel)
 721			@customer = customer
 722			@tel = tel
 723			@invites = InvitesRepo.new(DB, REDIS)
 724		end
 725
 726		def write
 727			return buy_number if @customer.balance < @tel.price
 728
 729			@tel.order(DB, @customer).then(
 730				->(_) { customer_active_tel_purchased },
 731				method(:number_purchase_error)
 732			)
 733		end
 734
 735	protected
 736
 737		def buy_number
 738			BuyNumber.new(@customer, @tel).write.then do
 739				reload_customer(@customer).then { |customer|
 740					Finish.new(customer, @tel)
 741				}.then(&:write)
 742			end
 743		end
 744
 745		def number_purchase_error(e)
 746			Command.log.error "number_purchase_error", e
 747			TEL_SELECTIONS.delete(@customer.jid).then {
 748				TEL_SELECTIONS[@customer.jid]
 749			}.then { |choose|
 750				choose.choose_tel_or_data(
 751					error: "The JMP number #{@tel} is no longer available."
 752				)
 753			}.then { |tel| Finish.new(@customer, tel).write }
 754		end
 755
 756		def raise_setup_error(e)
 757			Command.log.error "@customer.register! failed", e
 758			Command.finish(
 759				"There was an error setting up your number, " \
 760				"please contact JMP support.",
 761				type: :error
 762			)
 763		end
 764
 765		def put_default_fwd
 766			Bwmsgsv2Repo.new.put_fwd(@customer.customer_id, @tel.tel, CustomerFwd.for(
 767				uri: "xmpp:#{@customer.jid}",
 768				voicemail_enabled: true
 769			))
 770		end
 771
 772		def use_referral_code
 773			@invites.use_pending_group_code(@customer.customer_id).then do |credit_to|
 774				next unless credit_to
 775
 776				Transaction.new(
 777					customer_id: @customer.customer_id,
 778					transaction_id: "referral_#{@customer.customer_id}_#{credit_to}",
 779					amount: @customer.monthly_price,
 780					note: "Referral Bonus",
 781					bonus_eligible?: false
 782				).insert
 783			end
 784		end
 785
 786		def customer_active_tel_purchased
 787			@customer.register!(@tel.tel).catch(&method(:raise_setup_error)).then {
 788				EMPromise.all([
 789					TEL_SELECTIONS.delete(@customer.jid),
 790					put_default_fwd,
 791					use_referral_code,
 792					@tel.charge(@customer)
 793				])
 794			}.then do
 795				FinishOnboarding.for(@customer, @tel).then(&:write)
 796			end
 797		end
 798	end
 799
 800	module FinishOnboarding
 801		def self.for(customer, tel, db: LazyObject.new { DB })
 802			jid = ProxiedJID.new(customer.jid).unproxied
 803			if jid.domain == CONFIG[:onboarding_domain]
 804				Snikket.for(customer, tel, db: db)
 805			else
 806				NotOnboarding.new(customer, tel)
 807			end
 808		end
 809
 810		class Snikket
 811			def self.for(customer, tel, db:)
 812				::Snikket::Repo.new(db: db).find_by_customer(customer).then do |is|
 813					if is.empty?
 814						new(customer, tel, db: db)
 815					elsif is[0].bootstrap_token.empty?
 816						# This is a need_dns one, try the launch again
 817						new(customer, tel, db: db).launch(is[0].domain)
 818					else
 819						GetInvite.for(customer, is[0], tel, db: db)
 820					end
 821				end
 822			end
 823
 824			def initialize(customer, tel, error: nil, old: nil, db:)
 825				@customer = customer
 826				@tel = tel
 827				@error = error
 828				@db = db
 829				@old = old
 830			end
 831
 832			ACTION_VAR = "http://jabber.org/protocol/commands#actions"
 833
 834			def form
 835				FormTemplate.render(
 836					"registration/snikket",
 837					tel: @tel,
 838					error: @error
 839				)
 840			end
 841
 842			def write
 843				Command.reply { |reply|
 844					reply.allowed_actions = [:next]
 845					reply.command << form
 846				}.then(&method(:next_step))
 847			end
 848
 849			def next_step(iq)
 850				subdomain = empty_nil(iq.form.field("subdomain")&.value)
 851				domain = "#{subdomain}.snikket.chat"
 852				if iq.form.field(ACTION_VAR)&.value == "custom_domain"
 853					CustomDomain.new(@customer, @tel, old: @old).write
 854				elsif @old && (!subdomain || domain == @old.domain)
 855					GetInvite.for(@customer, @old, @tel, db: @db).then(&:write)
 856				else
 857					launch(domain)
 858				end
 859			end
 860
 861			def launch(domain)
 862				IQ_MANAGER.write(::Snikket::Launch.new(
 863					nil, CONFIG[:snikket_hosting_api], domain: domain
 864				)).then { |launched|
 865					save_instance_and_wait(domain, launched)
 866				}.catch { |e|
 867					next EMPromise.reject(e) unless e.respond_to?(:text)
 868
 869					Snikket.new(@customer, @tel, old: @old, error: e.text, db: @db).write
 870				}
 871			end
 872
 873			def save_instance_and_wait(domain, launched)
 874				instance = ::Snikket::CustomerInstance.for(@customer, domain, launched)
 875				repo = ::Snikket::Repo.new(db: @db)
 876				(@old&.domain == domain ? EMPromise.resolve(nil) : repo.del(@old))
 877					.then { repo.put(instance) }.then do
 878						if launched.status == :needs_dns
 879							NeedsDNS.new(@customer, instance, @tel, launched.records).write
 880						else
 881							GetInvite.for(@customer, instance, @tel, db: @db).then(&:write)
 882						end
 883					end
 884			end
 885
 886			def empty_nil(s)
 887				s.nil? || s.empty? ? nil : s
 888			end
 889
 890			class NeedsDNS < Snikket
 891				def initialize(customer, instance, tel, records, db: DB)
 892					@customer = customer
 893					@instance = instance
 894					@tel = tel
 895					@records = records
 896					@db = db
 897				end
 898
 899				def form
 900					FormTemplate.render(
 901						"registration/snikket_needs_dns",
 902						records: @records
 903					)
 904				end
 905
 906				def write
 907					Command.reply { |reply|
 908						reply.allowed_actions = [:prev, :next]
 909						reply.command << form
 910					}.then do |iq|
 911						if iq.prev?
 912							CustomDomain.new(@customer, @tel, old: @instance).write
 913						else
 914							launch(@instance.domain)
 915						end
 916					end
 917				end
 918			end
 919
 920			class GetInvite
 921				def self.for(customer, instance, tel, db: DB)
 922					instance.fetch_invite.then do |xmpp_uri|
 923						if xmpp_uri
 924							GoToInvite.new(xmpp_uri)
 925						else
 926							new(customer, instance, tel, db: db)
 927						end
 928					end
 929				end
 930
 931				def initialize(customer, instance, tel, db: DB)
 932					@customer = customer
 933					@instance = instance
 934					@tel = tel
 935					@db = db
 936				end
 937
 938				def form
 939					FormTemplate.render(
 940						"registration/snikket_wait",
 941						domain: @instance.domain
 942					)
 943				end
 944
 945				def write
 946					Command.reply { |reply|
 947						reply.allowed_actions = [:prev, :next]
 948						reply.command << form
 949					}.then do |iq|
 950						if iq.prev?
 951							Snikket.new(@customer, @tel, old: @instance, db: @db).write
 952						else
 953							GetInvite.for(@customer, @instance, @tel, db: @db).then(&:write)
 954						end
 955					end
 956				end
 957			end
 958
 959			class GoToInvite
 960				def initialize(xmpp_uri)
 961					@xmpp_uri = xmpp_uri
 962				end
 963
 964				def write
 965					Command.finish do |reply|
 966						oob = OOB.find_or_create(reply.command)
 967						oob.url = @xmpp_uri
 968					end
 969				end
 970			end
 971		end
 972
 973		class CustomDomain < Snikket
 974			def initialize(customer, tel, old: nil, error: nil, db: DB)
 975				@customer = customer
 976				@tel = tel
 977				@error = error
 978				@old = old
 979				@db = db
 980			end
 981
 982			def form
 983				FormTemplate.render(
 984					"registration/snikket_custom",
 985					tel: @tel,
 986					error: @error
 987				)
 988			end
 989
 990			def write
 991				Command.reply { |reply|
 992					reply.allowed_actions = [:prev, :next]
 993					reply.command << form
 994				}.then do |iq|
 995					if iq.prev?
 996						Snikket.new(@customer, @tel, db: @db, old: @old).write
 997					else
 998						launch(empty_nil(iq.form.field("domain")&.value) || @old&.domain)
 999					end
1000				end
1001			end
1002		end
1003
1004		class NotOnboarding
1005			def initialize(customer, tel)
1006				@customer = customer
1007				@tel = tel
1008			end
1009
1010			def write
1011				WelcomeMessage.new(@customer, @tel).welcome
1012				Command.finish("Your JMP account has been activated as #{@tel}")
1013			end
1014		end
1015	end
1016
1017	def self.public_onboarding_invite
1018		Command.finish { |reply|
1019			reply.allowed_actions = [:prev]
1020			oob = OOB.find_or_create(reply.command)
1021			oob.url = CONFIG[:public_onboarding_url]
1022			oob.desc = "Get your XMPP account"
1023		}
1024	end
1025end