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 ( - <> - - - + + { Object.keys( availableFields ).map( fieldKey => ( { @@ -296,8 +302,23 @@ const BillingFields = () => { } } /> ) ) } + { orderNotesField && ( + { + let newFields = [ ...billingFields ]; + if ( billingFields.includes( 'order_comments' ) ) { + newFields = newFields.filter( field => field !== 'order_comments' ); + } else { + newFields = [ ...newFields, 'order_comments' ]; + } + changeHandler( [ 'billingFields' ] )( newFields ); + } } + /> + ) } - + ); }; @@ -347,6 +368,7 @@ const Donation = () => { ) } +
@@ -1190,16 +1192,10 @@ function( $list ) {
- -
-
-

- -

-

- -

+
+ +
@@ -1216,7 +1212,6 @@ function( $list ) {
-

@@ -1376,6 +1371,12 @@ public static function render_third_party_auth() { $classnames = implode( ' ', [ $class(), $class() . '--disabled' ] ); ?>

+
@@ -1383,12 +1384,6 @@ public static function render_third_party_auth() {
-
ID; - } - } - return $customer_id; - } - - /** - * Don't force account registration/login on Woo purchases for existing users. - * - * @param array $data Array of Woo checkout data. - * @return array Modified $data. - */ - public static function dont_force_registration_for_existing_woo_users( $data ) { - $email = $data['billing_email']; - $customer = \get_user_by( 'email', $email ); - if ( $customer ) { - $data['createaccount'] = 0; - \add_filter( 'woocommerce_checkout_registration_required', '__return_false', 9999 ); - } - - return $data; - } } Reader_Activation::init(); diff --git a/includes/wizards/class-reader-revenue-wizard.php b/includes/wizards/class-reader-revenue-wizard.php index e13a26b287..789ca1ec65 100644 --- a/includes/wizards/class-reader-revenue-wizard.php +++ b/includes/wizards/class-reader-revenue-wizard.php @@ -453,13 +453,18 @@ public function fetch_all_data() { $stripe_data = Stripe_Connection::get_stripe_data(); $stripe_data['can_use_stripe_platform'] = Donations::can_use_stripe_platform(); - $billing_fields = []; + $billing_fields = null; + $order_notes_field = []; if ( $wc_installed && Donations::is_platform_wc() ) { - $checkout = new \WC_Checkout(); - $fields = $checkout->get_checkout_fields(); + $checkout = new \WC_Checkout(); + $fields = $checkout->get_checkout_fields(); + $checkout_fields = $fields; if ( ! empty( $fields['billing'] ) ) { $billing_fields = $fields['billing']; } + if ( ! empty( $fields['order']['order_comments'] ) ) { + $order_notes_field = $fields['order']['order_comments']; + } } $args = [ @@ -470,6 +475,7 @@ public function fetch_all_data() { 'donation_data' => Donations::get_donation_settings(), 'donation_page' => Donations::get_donation_page_info(), 'available_billing_fields' => $billing_fields, + 'order_notes_field' => $order_notes_field, 'salesforce_settings' => [], 'platform_data' => [ 'platform' => $platform,