diff --git a/services/web/frontend/js/main.js b/services/web/frontend/js/main.js
index 6e5fe076b8..d75e6a74fa 100644
--- a/services/web/frontend/js/main.js
+++ b/services/web/frontend/js/main.js
@@ -23,7 +23,6 @@ import './main/translations'
import './main/subscription-dashboard'
import './main/new-subscription'
import './main/annual-upgrade'
-import './main/register-users'
import './main/subscription/team-invite-controller'
import './main/subscription/upgrade-subscription'
import './main/learn'
diff --git a/services/web/frontend/js/main/register-users.js b/services/web/frontend/js/main/register-users.js
deleted file mode 100644
index d7e2296768..0000000000
--- a/services/web/frontend/js/main/register-users.js
+++ /dev/null
@@ -1,50 +0,0 @@
-import _ from 'lodash'
-/* eslint-disable
- max-len,
- no-return-assign,
-*/
-// TODO: This file was created by bulk-decaffeinate.
-// Fix any style issues and re-enable lint.
-/*
- * decaffeinate suggestions:
- * DS101: Remove unnecessary use of Array.from
- * DS102: Remove unnecessary code created because of implicit returns
- * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
- */
-import App from '../base'
-
-export default App.controller(
- 'RegisterUsersController',
- function ($scope, queuedHttp) {
- $scope.users = []
-
- $scope.inputs = { emails: '' }
-
- const parseEmails = function (emailsString) {
- const regexBySpaceOrComma = /[\s,]+/
- let emails = emailsString.split(regexBySpaceOrComma)
- emails = _.map(emails, email => (email = email.trim()))
- emails = _.filter(emails, email => email.indexOf('@') !== -1)
- return emails
- }
-
- return ($scope.registerUsers = function () {
- const emails = parseEmails($scope.inputs.emails)
- $scope.error = false
- return Array.from(emails).map(email =>
- queuedHttp
- .post('/admin/register', {
- email,
- _csrf: window.csrfToken,
- })
- .then(function (response) {
- const { data } = response
- const user = data
- $scope.users.push(user)
- return ($scope.inputs.emails = '')
- })
- .catch(() => ($scope.error = true))
- )
- })
- }
-)
diff --git a/services/web/modules/user-activate/app/views/user/register.pug b/services/web/modules/user-activate/app/views/user/register.pug
index 3dc0921a06..faca16ea9b 100644
--- a/services/web/modules/user-activate/app/views/user/register.pug
+++ b/services/web/modules/user-activate/app/views/user/register.pug
@@ -1,40 +1,11 @@
extends ../../../../../app/views/layout
+block entrypointVar
+ - entrypoint = 'modules/user-activate/pages/user-activate-page'
+
+
block content
.content.content-alt
.container
- .row
- .col-md-12
- .card(ng-controller="RegisterUsersController")
- .page-header
- h1 Register New Users
- form.form
- .row
- .col-md-4.col-xs-8
- input.form-control(
- name="email",
- type="text",
- placeholder="jane@example.com, joe@example.com",
- ng-model="inputs.emails",
- on-enter="registerUsers()"
- )
- .col-md-8.col-xs-4
- button.btn.btn-primary(ng-click="registerUsers()") #{translate("register")}
-
- .row-spaced(ng-show="error").ng-cloak.text-danger
- p Sorry, an error occured
-
- .row-spaced(ng-show="users.length > 0").ng-cloak.text-success
- p We've sent out welcome emails to the registered users.
- p You can also manually send them URLs below to allow them to reset their password and log in for the first time.
- p (Password reset tokens will expire after one week and the user will need registering again).
-
- hr(ng-show="users.length > 0").ng-cloak
- table(ng-show="users.length > 0").table.table-striped.ng-cloak
- tr
- th #{translate("email")}
- th Set Password Url
- tr(ng-repeat="user in users")
- td {{ user.email }}
- td(style="word-break: break-all;") {{ user.setNewPasswordUrl }}
-
\ No newline at end of file
+ #user-activate-register-container
+
\ No newline at end of file
diff --git a/services/web/modules/user-activate/frontend/js/components/register-form.js b/services/web/modules/user-activate/frontend/js/components/register-form.js
new file mode 100644
index 0000000000..0fc8211af6
--- /dev/null
+++ b/services/web/modules/user-activate/frontend/js/components/register-form.js
@@ -0,0 +1,87 @@
+import PropTypes from 'prop-types'
+import { postJSON } from '../../../../../frontend/js/infrastructure/fetch-json'
+
+function RegisterForm({
+ setRegistrationSuccess,
+ setEmails,
+ setRegisterError,
+ setFailedEmails,
+}) {
+ function handleRegister(event) {
+ event.preventDefault()
+ const formData = new FormData(event.target)
+ const formDataAsEntries = formData.entries()
+ const formDataAsObject = Object.fromEntries(formDataAsEntries)
+ const emailString = formDataAsObject.email
+ setRegistrationSuccess(false)
+ setRegisterError(false)
+ setEmails([])
+ registerGivenUsers(parseEmails(emailString))
+ }
+
+ async function registerGivenUsers(emails) {
+ const registeredEmails = []
+ const failingEmails = []
+ for (const email of emails) {
+ try {
+ const result = await registerUser(email)
+ registeredEmails.push(result)
+ } catch {
+ failingEmails.push(email)
+ }
+ }
+ if (registeredEmails.length > 0) setRegistrationSuccess(true)
+ if (failingEmails.length > 0) {
+ setRegisterError(true)
+ setFailedEmails(failingEmails)
+ }
+ setEmails(registeredEmails)
+ }
+
+ function registerUser(email) {
+ const options = { email }
+ const url = `/admin/register`
+ return postJSON(url, { body: options })
+ }
+
+ return (
+
+ )
+}
+
+function parseEmails(emailsText) {
+ const regexBySpaceOrComma = /[\s,]+/
+ let emails = emailsText.split(regexBySpaceOrComma)
+ emails.map(email => email.trim())
+ emails = emails.filter(email => email.indexOf('@') !== -1)
+ return emails
+}
+
+RegisterForm.propTypes = {
+ setRegistrationSuccess: PropTypes.func,
+ setEmails: PropTypes.func,
+ setRegisterError: PropTypes.func,
+ setFailedEmails: PropTypes.func,
+}
+
+export default RegisterForm
diff --git a/services/web/modules/user-activate/frontend/js/components/user-activate-register.js b/services/web/modules/user-activate/frontend/js/components/user-activate-register.js
new file mode 100644
index 0000000000..d3c66c8f59
--- /dev/null
+++ b/services/web/modules/user-activate/frontend/js/components/user-activate-register.js
@@ -0,0 +1,92 @@
+import { useState } from 'react'
+import PropTypes from 'prop-types'
+import RegisterForm from './register-form'
+function UserActivateRegister() {
+ const [emails, setEmails] = useState([])
+ const [failedEmails, setFailedEmails] = useState([])
+ const [registerError, setRegisterError] = useState(false)
+ const [registrationSuccess, setRegistrationSuccess] = useState(false)
+
+ return (
+
+
+
+
+
Register New Users
+
+
+ {registerError ? (
+
+ ) : null}
+ {registrationSuccess ? (
+ <>
+
+
+
+ >
+ ) : null}
+
+
+
+ )
+}
+
+function UserActivateError({ failedEmails }) {
+ return (
+
+
Sorry, an error occured, failed to register these emails.
+ {failedEmails.map(email => (
+
{email}
+ ))}
+
+ )
+}
+
+function SuccessfulRegistrationMessage() {
+ return (
+
+
We've sent out welcome emails to the registered users.
+
+ You can also manually send them URLs below to allow them to reset their
+ password and log in for the first time.
+
+
+ (Password reset tokens will expire after one week and the user will need
+ registering again).
+
+
+ )
+}
+
+function DisplayEmailsList({ emails }) {
+ return (
+
+
+
+ | Email |
+ Set Password Url |
+
+ {emails.map(user => (
+
+ | {user.email} |
+ {user.setNewPasswordUrl} |
+
+ ))}
+
+
+ )
+}
+
+DisplayEmailsList.propTypes = {
+ emails: PropTypes.array,
+}
+UserActivateError.propTypes = {
+ failedEmails: PropTypes.array,
+}
+
+export default UserActivateRegister
diff --git a/services/web/modules/user-activate/frontend/js/pages/user-activate-page.js b/services/web/modules/user-activate/frontend/js/pages/user-activate-page.js
new file mode 100644
index 0000000000..b7d9a69959
--- /dev/null
+++ b/services/web/modules/user-activate/frontend/js/pages/user-activate-page.js
@@ -0,0 +1,9 @@
+import '../../../../../frontend/js/marketing'
+
+import ReactDOM from 'react-dom'
+import UserActivateRegister from '../components/user-activate-register'
+
+ReactDOM.render(
+ ,
+ document.getElementById('user-activate-register-container')
+)
diff --git a/services/web/modules/user-activate/test/frontend/js/components/register-form.test.js b/services/web/modules/user-activate/test/frontend/js/components/register-form.test.js
new file mode 100644
index 0000000000..39c6e98c1b
--- /dev/null
+++ b/services/web/modules/user-activate/test/frontend/js/components/register-form.test.js
@@ -0,0 +1,62 @@
+import { expect } from 'chai'
+import { render, screen, fireEvent } from '@testing-library/react'
+import fetchMock from 'fetch-mock'
+import sinon from 'sinon'
+import RegisterForm from '../../../../frontend/js/components/register-form'
+
+describe('RegisterForm', function () {
+ beforeEach(function () {
+ fetchMock.reset()
+ })
+ afterEach(function () {
+ fetchMock.reset()
+ })
+ it('should render the register form', async function () {
+ const setRegistrationSuccessStub = sinon.stub()
+ const setEmailsStub = sinon.stub()
+ const setRegisterErrorStub = sinon.stub()
+ const setFailedEmailsStub = sinon.stub()
+
+ render(
+
+ )
+ await screen.findByLabelText('emails to register')
+ screen.getByRole('button', { name: /register/i })
+ })
+
+ it('should call the fetch request when register button is pressed', async function () {
+ const email = 'abc@gmail.com'
+ const setRegistrationSuccessStub = sinon.stub()
+ const setEmailsStub = sinon.stub()
+ const setRegisterErrorStub = sinon.stub()
+ const setFailedEmailsStub = sinon.stub()
+
+ const endPointResponse = {
+ status: 200,
+ body: {
+ email,
+ setNewPasswordUrl: 'SetNewPasswordURL',
+ },
+ }
+ const registerMock = fetchMock.post('/admin/register', endPointResponse)
+
+ render(
+
+ )
+ const registerInput = screen.getByLabelText('emails to register')
+ const registerButton = screen.getByRole('button', { name: /register/i })
+ fireEvent.change(registerInput, { target: { value: email } })
+ fireEvent.click(registerButton)
+ expect(registerMock.called()).to.be.true
+ })
+})
diff --git a/services/web/modules/user-activate/test/frontend/js/components/user-activate-register.test.js b/services/web/modules/user-activate/test/frontend/js/components/user-activate-register.test.js
new file mode 100644
index 0000000000..351336a17e
--- /dev/null
+++ b/services/web/modules/user-activate/test/frontend/js/components/user-activate-register.test.js
@@ -0,0 +1,139 @@
+import { expect } from 'chai'
+import { render, screen, fireEvent } from '@testing-library/react'
+import fetchMock from 'fetch-mock'
+import UserActivateRegister from '../../../../frontend/js/components/user-activate-register'
+
+describe('UserActivateRegister', function () {
+ beforeEach(function () {
+ fetchMock.reset()
+ })
+ afterEach(function () {
+ fetchMock.reset()
+ })
+ it('should display the error message', async function () {
+ const email = 'abc@gmail.com'
+ render()
+ const endPointResponse = {
+ status: 500,
+ }
+ const registerMock = fetchMock.post('/admin/register', endPointResponse)
+ const registerInput = screen.getByLabelText('emails to register')
+ const registerButton = screen.getByRole('button', { name: /register/i })
+
+ fireEvent.change(registerInput, { target: { value: email } })
+ fireEvent.click(registerButton)
+
+ expect(registerMock.called()).to.be.true
+ await screen.findByText('Sorry, an error occured', { exact: false })
+ })
+
+ it('should display the success message', async function () {
+ const email = 'abc@gmail.com'
+ render()
+ const endPointResponse = {
+ status: 200,
+ body: {
+ email,
+ setNewPasswordUrl: 'SetNewPasswordURL',
+ },
+ }
+ const registerMock = fetchMock.post('/admin/register', endPointResponse)
+ const registerInput = screen.getByLabelText('emails to register')
+ const registerButton = screen.getByRole('button', { name: /register/i })
+
+ fireEvent.change(registerInput, { target: { value: email } })
+ fireEvent.click(registerButton)
+
+ expect(registerMock.called()).to.be.true
+ await screen.findByText(
+ "We've sent out welcome emails to the registered users."
+ )
+ })
+
+ it('should display the registered emails', async function () {
+ const email = 'abc@gmail.com, def@gmail.com'
+ render()
+ const endPointResponse1 = {
+ status: 200,
+ body: {
+ email: 'abc@gmail.com',
+ setNewPasswordUrl: 'SetNewPasswordURL',
+ },
+ }
+ const endPointResponse2 = {
+ status: 200,
+ body: {
+ email: 'def@gmail.com',
+ setNewPasswordUrl: 'SetNewPasswordURL',
+ },
+ }
+ const registerMock = fetchMock.post('/admin/register', (path, req) => {
+ const body = JSON.parse(req.body)
+ if (body.email === 'abc@gmail.com') return endPointResponse1
+ else if (body.email === 'def@gmail.com') return endPointResponse2
+ })
+ const registerInput = screen.getByLabelText('emails to register')
+ const registerButton = screen.getByRole('button', { name: /register/i })
+
+ fireEvent.change(registerInput, { target: { value: email } })
+ fireEvent.click(registerButton)
+
+ expect(registerMock.called()).to.be.true
+ await screen.findByText('abc@gmail.com')
+ await screen.findByText('def@gmail.com')
+ })
+
+ it('should display the failed emails', async function () {
+ const email = 'abc@, def@'
+ render()
+ const endPointResponse1 = {
+ status: 500,
+ }
+ const endPointResponse2 = {
+ status: 500,
+ }
+ const registerMock = fetchMock.post('/admin/register', (path, req) => {
+ const body = JSON.parse(req.body)
+ if (body.email === 'abc@') return endPointResponse1
+ else if (body.email === 'def@') return endPointResponse2
+ })
+ const registerInput = screen.getByLabelText('emails to register')
+ const registerButton = screen.getByRole('button', { name: /register/i })
+
+ fireEvent.change(registerInput, { target: { value: email } })
+ fireEvent.click(registerButton)
+
+ expect(registerMock.called()).to.be.true
+ await screen.findByText('abc@')
+ await screen.findByText('def@')
+ })
+
+ it('should display the registered and failed emails together', async function () {
+ const email = 'abc@gmail.com, def@'
+ render()
+ const endPointResponse1 = {
+ status: 200,
+ body: {
+ email: 'abc@gmail.com',
+ setNewPasswordUrl: 'SetNewPasswordURL',
+ },
+ }
+ const endPointResponse2 = {
+ status: 500,
+ }
+ const registerMock = fetchMock.post('/admin/register', (path, req) => {
+ const body = JSON.parse(req.body)
+ if (body.email === 'abc@gmail.com') return endPointResponse1
+ else if (body.email === 'def@gmail.com') return endPointResponse2
+ })
+ const registerInput = screen.getByLabelText('emails to register')
+ const registerButton = screen.getByRole('button', { name: /register/i })
+
+ fireEvent.change(registerInput, { target: { value: email } })
+ fireEvent.click(registerButton)
+
+ expect(registerMock.called()).to.be.true
+ await screen.findByText('abc@gmail.com')
+ await screen.findByText('def@')
+ })
+})