diff --git a/assets/reader-activation/auth.js b/assets/reader-activation/auth.js
index 312f7c53d8..6287e4ee67 100644
--- a/assets/reader-activation/auth.js
+++ b/assets/reader-activation/auth.js
@@ -27,6 +27,17 @@ function domReady( callback ) {
document.addEventListener( 'DOMContentLoaded', callback );
}
+/**
+ * Format time in MM:SS format.
+ *
+ * @param {number} time Time in seconds.
+ */
+function formatTime( time ) {
+ const minutes = Math.floor( time / 60 );
+ const seconds = time % 60;
+ return `${ minutes }:${ seconds < 10 ? '0' : '' }${ seconds }`;
+}
+
/**
* Converts FormData into an object.
*
@@ -51,6 +62,7 @@ const convertFormDataToObject = ( formData, includedFields = [] ) =>
}, {} );
const SIGN_IN_MODAL_HASHES = [ 'signin_modal', 'register_modal' ];
+
let currentHash;
window.newspackRAS = window.newspackRAS || [];
@@ -215,6 +227,92 @@ window.newspackRAS.push( function ( readerActivation ) {
const passwordInput = form.querySelector( 'input[name="password"]' );
const submitButtons = form.querySelectorAll( '[type="submit"]' );
const closeButton = container.querySelector( 'button[data-close]' );
+ const backButtons = container.querySelectorAll( '[data-back]' );
+ const resendCodeButton = container.querySelector( '[data-resend-code]' );
+
+ backButtons.forEach( backButton => {
+ backButton.addEventListener( 'click', function ( ev ) {
+ ev.preventDefault();
+ setFormAction( 'link', true );
+ } );
+ } );
+
+ let otpTimerInterval;
+ let otpOriginalButtonText;
+ function handleOTPTimer() {
+ if ( otpTimerInterval ) {
+ clearInterval( otpTimerInterval );
+ }
+ if ( ! resendCodeButton ) {
+ return;
+ }
+ otpOriginalButtonText = resendCodeButton.textContent;
+ const updateButton = () => {
+ const remaining = readerActivation.getOTPTimeRemaining();
+ if ( remaining ) {
+ resendCodeButton.textContent = `${ otpOriginalButtonText } (${ formatTime(
+ remaining
+ ) })`;
+ resendCodeButton.disabled = true;
+ } else {
+ resendCodeButton.textContent = otpOriginalButtonText;
+ resendCodeButton.disabled = false;
+ clearInterval( otpTimerInterval );
+ }
+ };
+ const remaining = readerActivation.getOTPTimeRemaining();
+ if ( remaining ) {
+ otpTimerInterval = setInterval( updateButton, 1000 );
+ updateButton();
+ }
+ }
+
+ if ( resendCodeButton ) {
+ handleOTPTimer();
+ resendCodeButton.addEventListener( 'click', function ( ev ) {
+ messageContentElement.innerHTML = '';
+ ev.preventDefault();
+ form.startLoginFlow();
+ const body = new FormData();
+ body.set( 'reader-activation-auth-form', 1 );
+ body.set( 'npe', emailInput.value );
+ body.set( 'action', 'link' );
+ readerActivation
+ .getCaptchaToken()
+ .then( captchaToken => {
+ if ( ! captchaToken ) {
+ return;
+ }
+ body.set( 'captcha_token', captchaToken );
+ } )
+ .catch( e => {
+ console.log( { e } );
+ } )
+ .finally( () => {
+ fetch( form.getAttribute( 'action' ) || window.location.pathname, {
+ method: 'POST',
+ headers: {
+ Accept: 'application/json',
+ },
+ body,
+ } )
+ .then( () => {
+ messageContentElement.innerHTML = newspack_reader_auth_labels.code_resent;
+ readerActivation.setOTPTimer();
+ } )
+ .catch( e => {
+ console.log( e );
+ } )
+ .finally( () => {
+ handleOTPTimer();
+ form.style.opacity = 1;
+ submitButtons.forEach( button => {
+ button.disabled = false;
+ } );
+ } );
+ } );
+ } );
+ }
if ( closeButton ) {
closeButton.addEventListener( 'click', function ( ev ) {
@@ -250,6 +348,15 @@ window.newspackRAS.push( function ( readerActivation ) {
if ( ! readerActivation.getOTPHash() ) {
return;
}
+ const emailAddressElements = container.querySelectorAll( '.email-address' );
+ emailAddressElements.forEach( element => {
+ element.textContent = readerActivation.getReader()?.email || '';
+ } );
+ // Focus on the first input.
+ const firstInput = container.querySelector( '.otp-field input[type="text"]' );
+ if ( firstInput ) {
+ firstInput.focus();
+ }
}
if ( [ 'link', 'pwd' ].includes( action ) ) {
readerActivation.setAuthStrategy( action );
@@ -430,6 +537,9 @@ window.newspackRAS.push( function ( readerActivation ) {
const otpHash = readerActivation.getOTPHash();
if ( otpHash && [ 'register', 'link' ].includes( action ) ) {
if ( status === 200 ) {
+ // Set OTP rate-limit timer
+ readerActivation.setOTPTimer();
+ handleOTPTimer();
setFormAction( 'otp' );
}
/** If action is link, suppress message and status so the OTP handles it. */
diff --git a/assets/reader-activation/auth.scss b/assets/reader-activation/auth.scss
index 90c247ab6c..c587c43477 100644
--- a/assets/reader-activation/auth.scss
+++ b/assets/reader-activation/auth.scss
@@ -153,6 +153,22 @@
}
}
+ button[type='button'] {
+ display: block;
+ background: transparent;
+ border: 1px solid wp-colors.$gray-200;
+ color: wp-colors.$gray-700;
+ margin-bottom: 0.8rem;
+ &:disabled {
+ background-color: wp-colors.$gray-100;
+ color: wp-colors.$gray-600;
+ }
+ &.back-button {
+ border: 0;
+ margin: 0;
+ }
+ }
+
.components-form {
&__field {
font-size: 1rem;
@@ -168,6 +184,7 @@
background-color: var( --newspack-cta-color );
color: var( --newspack-cta-contrast-color );
transition: background-color 150ms ease-in-out;
+ margin-bottom: 0.8rem;
&:hover,
&:focus {
@@ -327,8 +344,6 @@
}
&__response {
- font-size: 0.8125em;
- line-height: 1.2307;
&__content {
p {
margin: 0;
diff --git a/assets/reader-activation/index.js b/assets/reader-activation/index.js
index a5343391e7..f439f072b0 100644
--- a/assets/reader-activation/index.js
+++ b/assets/reader-activation/index.js
@@ -144,6 +144,43 @@ export function getOTPHash() {
return getCookie( 'np_otp_hash' );
}
+/**
+ * OTP timer storage key.
+ */
+const OTP_TIMER_STORAGE_KEY = 'newspack_otp_timer';
+
+/**
+ * Set the OTP timer to the current time.
+ */
+export function setOTPTimer() {
+ localStorage.setItem( OTP_TIMER_STORAGE_KEY, Math.floor( Date.now() / 1000 ) );
+}
+
+/**
+ * Clear the OTP timer.
+ */
+export function clearOTPTimer() {
+ localStorage.removeItem( OTP_TIMER_STORAGE_KEY );
+}
+
+/**
+ * Get the time remaining for the OTP timer.
+ *
+ * @return {number} Time remaining in seconds
+ */
+export function getOTPTimeRemaining() {
+ const timer = localStorage.getItem( OTP_TIMER_STORAGE_KEY );
+ if ( ! timer ) {
+ return 0;
+ }
+ const timeRemaining =
+ newspack_ras_config.otp_rate_interval - ( Math.floor( Date.now() / 1000 ) - timer );
+ if ( ! timeRemaining ) {
+ clearOTPTimer();
+ }
+ return timeRemaining > 0 ? timeRemaining : 0;
+}
+
/**
* Authenticate reader using an OTP code.
*
@@ -338,6 +375,9 @@ const readerActivation = {
getReader,
hasAuthLink,
getOTPHash,
+ setOTPTimer,
+ clearOTPTimer,
+ getOTPTimeRemaining,
authenticateOTP,
setAuthStrategy,
getAuthStrategy,
diff --git a/assets/wizards/readerRevenue/views/donation/index.tsx b/assets/wizards/readerRevenue/views/donation/index.tsx
index 03be8f4297..3efc0f900c 100644
--- a/assets/wizards/readerRevenue/views/donation/index.tsx
+++ b/assets/wizards/readerRevenue/views/donation/index.tsx
@@ -41,6 +41,16 @@ const FREQUENCIES: {
};
const FREQUENCY_SLUGS: FrequencySlug[] = Object.keys( FREQUENCIES ) as FrequencySlug[];
+type FieldConfig = {
+ autocomplete: string;
+ class: string[];
+ label: string;
+ priority: number;
+ required: boolean;
+ type: string;
+ validate: string[];
+};
+
type WizardData = {
donation_data:
| { errors: { [ key: string ]: string[] } }
@@ -61,16 +71,9 @@ type WizardData = {
status: string;
};
available_billing_fields: {
- [ key: string ]: {
- autocomplete: string;
- class: string[];
- label: string;
- priority: number;
- required: boolean;
- type: string;
- validate: string[];
- };
+ [ key: string ]: FieldConfig;
};
+ order_notes_field: FieldConfig;
};
export const DonationAmounts = () => {
@@ -258,6 +261,8 @@ const BillingFields = () => {
} );
const availableFields = wizardData.available_billing_fields;
+ const orderNotesField = wizardData.order_notes_field;
+ console.log( wizardData );
if ( ! availableFields || ! Object.keys( availableFields ).length ) {
return null;
}
@@ -267,22 +272,23 @@ const BillingFields = () => {
: Object.keys( availableFields );
return (
- <>
-