diff --git a/atoms.scss b/atoms.scss index d5a0b6c4..73463eba 100644 --- a/atoms.scss +++ b/atoms.scss @@ -13,13 +13,17 @@ color: currentColor; font: 400 18px var(--nfd-ecommerce-font-primary); } + a { + color: var(--nfd-ecommerce-accent-primary); + text-decoration: none; + } .nfd-ecommerce-loader-mini { width: 1em; height: 1em; margin: 0; } .nfd-ecommerce-loader-inverse { - background: linear-gradient(to right, #f1f5f7 5%, rgba(0,0,0,0) 32%); + background: linear-gradient(to right, #f1f5f7 5%, rgba(0, 0, 0, 0) 32%); &:before { background: #f1f5f7; } diff --git a/build/index.asset.php b/build/index.asset.php index db1b5b98..2d4bbb2e 100644 --- a/build/index.asset.php +++ b/build/index.asset.php @@ -1 +1 @@ - array('react', 'wp-api-fetch', 'wp-components', 'wp-compose', 'wp-element', 'wp-i18n', 'wp-primitives'), 'version' => '8b69f94ca7bf0d2ddc2f'); + array('react', 'wp-api-fetch', 'wp-components', 'wp-compose', 'wp-element', 'wp-i18n', 'wp-primitives'), 'version' => 'c737810d340adc0b0f21'); diff --git a/includes/Data/Brands.php b/includes/Data/Brands.php new file mode 100644 index 00000000..8b9b2073 --- /dev/null +++ b/includes/Data/Brands.php @@ -0,0 +1,62 @@ + array( + 'brand' => 'crazy-domains', + 'name' => 'CrazyDomains', + 'url' => 'https://crazydomains.com', + 'hireExpertsInfo' => 'admin.php?page=crazy-domains#/marketplace/services/blue-sky', + 'adminPage' => 'admin.php?page=crazy-domains', + 'setup' => array( + 'payment' => array('Paypal'), + 'shipping' => array('Shippo'), + ), + 'defaultContact' => array( + 'woocommerce_default_country' => 'AU:NSW', + 'woocommerce_currency' => 'AUD', + ), + ), + 'bluehost' => array( + 'brand' => 'bluehost', + 'name' => 'Bluehost', + 'url' => 'https://bluehost.com', + 'hireExpertsInfo' => 'admin.php?page=bluehost#/marketplace/services/blue-sky', + 'adminPage' => 'admin.php?page=bluehost', + 'setup' => array( + 'payment' => array('Paypal'), + 'shipping' => array('Shippo'), + ), + 'defaultContact' => array( + 'woocommerce_default_country' => 'US:AZ', + 'woocommerce_currency' => 'USD', + ), + ), + 'bluehost-india' => array( + 'brand' => 'bluehost-india', + 'name' => 'Bluehost', + 'url' => 'https://bluehost.in', + 'hireExpertsInfo' => 'https://www.bluehost.in/solutions/full-service', + 'adminPage' => 'admin.php?page=bluehost', + 'setup' => array( + 'payment' => array('Razorpay'), + 'shipping' => array(), + ), + 'defaultContact' => array( + 'woocommerce_default_country' => 'IN:AP', + 'woocommerce_currency' => 'INR', + ), + ), + ); + } +} diff --git a/includes/Data/Plugins.php b/includes/Data/Plugins.php index 2ccab1dc..fdfd8b8b 100644 --- a/includes/Data/Plugins.php +++ b/includes/Data/Plugins.php @@ -17,6 +17,7 @@ public static function get_slug_map( $plugin ) { 'yith_shippo_shipping_for_woocommerce' => array( 'https://hiive.cloud/workers/plugin-downloads/yith-shippo-shippings-for-woocommerce', 'yith-shippo-shippings-for-woocommerce-extended/init.php' ), 'yith_paypal_payments' => array( 'https://hiive.cloud/workers/plugin-downloads/yith-paypal-payments-for-woocommerce', 'yith-paypal-payments-for-woocommerce-extended/init.php' ), 'woocommerce' => array( 'ignore', 'woocommerce/woocommerce.php' ), + 'woo_razorpay' => array( 'ignore', 'woo-razorpay/woo-razorpay.php' ), ); if ( in_array( $plugin, array_keys( $map ) ) ) { $plugin = $map[ $plugin ]; diff --git a/includes/ECommerce.php b/includes/ECommerce.php index b7293fba..ffca6440 100644 --- a/includes/ECommerce.php +++ b/includes/ECommerce.php @@ -21,6 +21,7 @@ class ECommerce { protected $options = array( 'nfd-ecommerce-captive-flow-paypal', 'nfd-ecommerce-captive-flow-shippo', + 'nfd-ecommerce-captive-flow-razorpay', 'nfd-ecommerce-onboarding-check', 'nfd-ecommerce-counter', 'woocommerce_store_address', diff --git a/includes/Partials/CaptiveFlow.php b/includes/Partials/CaptiveFlow.php index 7ec22795..01ca452d 100644 --- a/includes/Partials/CaptiveFlow.php +++ b/includes/Partials/CaptiveFlow.php @@ -5,10 +5,57 @@ class CaptiveFlow { + static $PAYPAL_CAPTIVE_FLOW = 'nfd-ecommerce-captive-flow-paypal'; + static $SHIPPO_CAPTIVE_FLOW = 'nfd-ecommerce-captive-flow-shippo'; + static $RAZORPAY_CAPTIVE_FLOW = 'nfd-ecommerce-captive-flow-razorpay'; + public static function init() { add_action( 'admin_menu', array( __CLASS__, 'register_page' ) ); - add_action( 'load-admin_page_' . 'nfd-ecommerce-captive-flow-paypal', array( __CLASS__, 'enqueue_styles' ), 100 ); - add_action( 'load-admin_page_' . 'nfd-ecommerce-captive-flow-shippo', array( __CLASS__, 'enqueue_styles' ), 100 ); + add_action( 'rest_api_init', array( __CLASS__, 'register_options' ) ); + add_action( 'load-admin_page_' . self::$PAYPAL_CAPTIVE_FLOW, array( __CLASS__, 'enqueue_styles' ), 100 ); + add_action( 'load-admin_page_' . self::$SHIPPO_CAPTIVE_FLOW, array( __CLASS__, 'enqueue_styles' ), 100 ); + } + + public static function register_options() { + \register_setting( + 'general', + 'woocommerce_razorpay_settings', + array( + 'show_in_rest' => array( + 'schema' => array( + 'type' => 'object', + 'properties' => array( + 'enabled' => array( + 'type' => 'string', + ), + 'title' => array( + 'type' => 'string', + ), + 'description' => array( + 'type' => 'string', + ), + 'key_id' => array( + 'type' => 'string', + ), + 'key_secret' => array( + 'type' => 'string', + ), + 'payment_action' => array( + 'type' => 'string', + ), + 'order_success_message' => array( + 'type' => 'string', + ), + 'enable_1cc_debug_mode' => array( + 'type' => 'string', + ), + ), + ), + ), + 'type' => 'object', + 'description' => __( 'NFD eCommerce RazorPay Options', 'wp-module-ecommerce' ), + ) + ); } public static function enqueue_styles() { @@ -27,7 +74,7 @@ public static function register_page() { null, null, Permissions::ADMIN, - 'nfd-ecommerce-captive-flow-paypal', + self::$PAYPAL_CAPTIVE_FLOW, array( __CLASS__, 'render_paypal' ), 100 ); @@ -36,7 +83,7 @@ public static function register_page() { null, null, Permissions::ADMIN, - 'nfd-ecommerce-captive-flow-shippo', + self::$SHIPPO_CAPTIVE_FLOW, array( __CLASS__, 'render_shippo' ), 100 ); @@ -45,7 +92,7 @@ public static function register_page() { public static function render_paypal() { echo PHP_EOL; echo '
'; - echo do_action( 'nfd-ecommerce-captive-flow-paypal' ); + echo do_action( self::$PAYPAL_CAPTIVE_FLOW ); echo '
'; echo PHP_EOL; } @@ -53,7 +100,7 @@ public static function render_paypal() { public static function render_shippo() { echo PHP_EOL; echo '
'; - echo do_action( 'nfd-ecommerce-captive-flow-shippo' ); + echo do_action( self::$SHIPPO_CAPTIVE_FLOW ); echo '
'; echo PHP_EOL; } diff --git a/includes/RestApi/PluginsController.php b/includes/RestApi/PluginsController.php index 250f82c5..ad7155e3 100644 --- a/includes/RestApi/PluginsController.php +++ b/includes/RestApi/PluginsController.php @@ -54,6 +54,7 @@ public function get_plugins_status() { 'yith_wcas_panel', 'yith_paypal_payments', 'yith_shippo_shipping_for_woocommerce', + 'woo_razorpay' ); foreach ( $plugins as $plugin ) { $map = Plugins::get_slug_map( $plugin ); diff --git a/includes/RestApi/UserController.php b/includes/RestApi/UserController.php index 6cb2c1c4..dd16ca5f 100644 --- a/includes/RestApi/UserController.php +++ b/includes/RestApi/UserController.php @@ -5,6 +5,7 @@ use NewfoldLabs\WP\Module\ECommerce\Permissions; use function NewfoldLabs\WP\ModuleLoader\container; use NewfoldLabs\WP\Module\Onboarding\Data\Data; +use NewfoldLabs\WP\Module\ECommerce\Data\Brands; class UserController { @@ -34,21 +35,25 @@ public function get_page_status() { ); $pages = \get_pages( $args ); $theme = \wp_get_theme(); - $brand = 'newfold'; + $brand_raw_value = \get_option('mm_brand', 'newfold' ); + $brand = \sanitize_title_with_dashes( strtolower( str_replace( '_', '-', $brand_raw_value ) ) ); $customer = array( - plan_subtype => 'wc_premium' + 'plan_subtype' => 'unknown' ); + $brands = Brands::get_brands(); + $currentBrandConfig = $brands[$brand]; if (class_exists('NewfoldLabs\WP\Module\Onboarding\Data\Data')) { - $brand_details = Data::current_brand(); - $brand = $brand_details['brand']; $customer_from_options = Data::customer_data(); if ($customer_from_options != false) { $customer = $customer_from_options; } } return array( + 'site' => array ( + 'url' => \get_site_url() + ), 'details' => $customer, - 'brand' => $brand, + 'currentBrandConfig' => $currentBrandConfig, 'theme' => array( 'manage' => Permissions::rest_can_manage_themes(), 'template' => $theme->get_template(), diff --git a/modals.scss b/modals.scss index e77c1af3..1cdf6966 100644 --- a/modals.scss +++ b/modals.scss @@ -127,3 +127,15 @@ text-decoration: none; } } + +form.nfd-ecommerce-modal-razorpay { + display: grid; + padding: 40px 40px 0.3em; + gap: 1.5em; + @media screen and (max-width: 481px) { + padding: 40px 1em 0.3em; + .razorpay-logo { + justify-self: center; + } + } +} diff --git a/src/components/CaptiveRazorpay.js b/src/components/CaptiveRazorpay.js new file mode 100644 index 00000000..e3ea4225 --- /dev/null +++ b/src/components/CaptiveRazorpay.js @@ -0,0 +1,212 @@ +import { + Button, + ButtonGroup, + TextControl, + ToggleControl, +} from "@wordpress/components"; +import { useEffect, useState } from "@wordpress/element"; +import { sprintf, __ } from "@wordpress/i18n"; +import { ReactComponent as RazorPayBrand } from "../icons/razorpay-brand.svg"; +import { updateWPSettings } from "../services"; + +/** @type {((key: string) => boolean)[]} */ +const KeyChecks = [ + (key) => key?.startsWith("rzp_test_"), + (key) => key?.startsWith("rzp_live_"), +]; + +const Content = { + keyIdLabel: __("Key ID", "wp-module-ecommerce"), + keyTestIdLabel: __("Test Key ID", "wp-module-ecommerce"), + keySecretLabel: __("Key Secret", "wp-module-ecommerce"), + keyTestSecretLabel: __("Test Key Secret", "wp-module-ecommerce"), + keyId: sprintf( + __( + `The key ID can be found in the "%1$sAPI Keys%2$s" section of your Razorpay dashboard.`, + "wp-module-ecommerce" + ), + ``, + "" + ), + invalidTestKeyId: __( + "That is not a valid test key ID. Please check your key ID and enter it again.", + "wp-module-ecommerce" + ), + invalidProductionKeyId: __( + "That is not a valid production key ID. Please check your key ID and enter it again.", + "wp-module-ecommerce" + ), + keySecret: sprintf( + __( + `The key secret can be found in the "%1$sAPI Keys%2$s" section of your Razorpay dashboard.`, + "wp-module-ecommerce" + ), + ``, + "" + ), +}; + +const rzrPaySettings = { + enabled: "yes", + title: "Credit Card/Debit Card/NetBanking", + description: + "Pay securely by Credit or Debit card or Internet Banking through Razorpay.", + payment_action: "capture", + order_success_message: + "Thank you for shopping with us. Your account has been charged and your transaction is successful. We will be processing your order soon.", + enable_1cc_debug_mode: "yes", +}; + +export function CaptiveRazorpay({ onComplete, settings, hireExpertsUrl }) { + let [isTestMode, setTestMode] = useState(() => false); + let [rzrKeys, updateKeys] = useState({ + key_id: "", + key_secret: "", + }); + useEffect(() => { + if (settings) { + setTestMode(settings?.key_id?.startsWith("rzp_test_")); + updateKeys(settings); + } + }, [settings]); + let isFormDisabled = settings === undefined; + let [isTestKeyValid, isProductionKeyValid] = KeyChecks.map( + (check) => rzrKeys.key_id === "" || check(rzrKeys.key_id) + ); + let isKeyValid = isTestMode ? isTestKeyValid : isProductionKeyValid; + return ( +
{ + event.preventDefault(); + event.stopPropagation(); + await updateWPSettings({ + "nfd-ecommerce-captive-flow-razorpay": "true", + woocommerce_razorpay_settings: { ...rzrPaySettings, ...rzrKeys }, + }); + await onComplete(); + }} + > +

+ {__("Connect your Razorpay Account", "wp-module-ecommerce")} +

+ +
+ +

+ {__("Already have an account? ", "wp-module-ecommerce")} + + {__("Login", "wp-module-ecommerce")} + +

+

+ {__( + "After you successfully login and generate your key ID and key secret codes, you will then need to switch back to this tab in your browser and paste your codes into the fields below.", + "wp-module-ecommerce" + )} +

+
+
+ + + updateKeys((keys) => ({ ...keys, key_id }))} + required + __nextHasNoMarginBottom + label={isTestMode ? Content.keyTestIdLabel : Content.keyIdLabel} + placeholder={__( + `enter your ${isTestMode ? "test" : "production"} key ID here.`, + "wp-module-ecommerce" + )} + help={ + + } + /> + + + updateKeys((keys) => ({ ...keys, key_secret })) + } + required + __nextHasNoMarginBottom + label={ + isTestMode ? Content.keyTestSecretLabel : Content.keySecretLabel + } + placeholder={__( + `enter your ${isTestMode ? "test" : "production"} key secret here.`, + "wp-module-ecommerce" + )} + help={ + + } + /> +
+

+ * required +

+ + + + +

+ + {__("Need help?", "wp-module-ecommerce")}{" "} + + {__("Hire our experts", "wp-module-ecommerce")} + + +

+ + ); +} diff --git a/src/components/Dashboard.js b/src/components/Dashboard.js index d07f0a80..4e448dab 100644 --- a/src/components/Dashboard.js +++ b/src/components/Dashboard.js @@ -40,7 +40,7 @@ export function Dashboard(props) { let { key, StepContent } = guideSteps.find((step) => step.key === props.section) ?? guideSteps[0]; useSetupYITHWonderTheme(props.user); - let isCleanUpInProgress = useOnboardingCleanup(props.plugins.token?.hash); + let isCleanUpInProgress = useOnboardingCleanup(props.plugins.token?.hash, props.user); let addCurtain = props.plugins?.status?.woocommerce !== "Active"; function onBackButtonClick() { navigate("/home/store"); diff --git a/src/components/GeneralSettings.js b/src/components/GeneralSettings.js index 17d2b1be..a8133555 100644 --- a/src/components/GeneralSettings.js +++ b/src/components/GeneralSettings.js @@ -4,8 +4,8 @@ import { useCardManager } from "./useCardManager"; import { DashboardContent } from "./DashboardContent"; export function GeneralSettings(props) { - let { plugins } = props; - let cards = useCardManager(GeneralSettingsConfig(plugins), { + let { user, plugins } = props; + let cards = useCardManager(GeneralSettingsConfig(user, plugins), { refreshInterval: 8 * 1000, }); const isLoading = !(cards ?? []).every( diff --git a/src/components/MinimalCard.js b/src/components/MinimalCard.js index 808d0525..d26893cf 100644 --- a/src/components/MinimalCard.js +++ b/src/components/MinimalCard.js @@ -1,41 +1,71 @@ +import { Tooltip } from "@wordpress/components"; import { useEffect, useState } from "@wordpress/element"; import { ReactComponent as CompletedTask } from "../icons/task-completed-solid.svg"; +import { ReactComponent as InProgressTask } from "../icons/task-incomplete.svg"; import ModalCard from "./ModalCard"; +/** + * @typedef TaskStatusIndicatorProps + * @property {"complete" | "inprogress" | "pending"} status + * @property {string?} message Used for inprogress currently + * + * @param {TaskStatusIndicatorProps} props + */ +function TaskStatusIndicator({ status, message }) { + if (status === "complete") { + return ( +
+ +
+ ); + } + if (status === "inprogress") { + return ( + +
+ +
+
+ ); + } + return null; +} + export function MinimalCard(props) { const [showModal, setShowModal] = useState(null); - let modal = props.modal(); + let modal = props.modal(props.state); let { image: Icon } = props.assets(props.state); - const taskCompleted = props.state.taskCompleted; + + // TODO: Deprecate taskCompleted and use taskStatus only + const { taskCompleted, taskStatus = "pending" } = props.state; useEffect(() => { - if (taskCompleted && showModal) { + if ((taskCompleted || taskStatus === "complete") && showModal) { setShowModal(false); } - }, [taskCompleted, showModal]); - let { title, actionName } = props.text(taskCompleted); + }, [taskCompleted, taskStatus, showModal]); + + // TODO: We should pass complete state here. + let { + title, + actionName, + inProgressMessage = "", + } = props.text(taskCompleted, taskStatus); const buttonClickHandler = () => { - const data = { - taskCompleted: taskCompleted, - }; - props.actions.buttonClick(data, setShowModal); + props.actions.buttonClick(props.state, setShowModal); }; - return ( <> diff --git a/src/components/StoreAddress.js b/src/components/StoreAddress.js index 5da8d024..afad25fe 100644 --- a/src/components/StoreAddress.js +++ b/src/components/StoreAddress.js @@ -8,25 +8,28 @@ const CountriesInOFAC = ["CU", "KP", "IR", "RU", "SY", "AF", "BY", "MM", "VN"]; function useBasicProfile() { let { data: countries } = useSWR("/wc/v3/data/countries"); let { data: currencies } = useSWR("/wc/v3/data/currencies"); - let defaultContact = { - country: "US", - state: "AZ", - woocommerce_store_address: "", - woocommerce_store_address_2: "", - woocommerce_store_city: "", - woocommerce_store_postcode: "", - }; return [ !countries || !currencies, - defaultContact, countries?.filter((_) => !CountriesInOFAC.includes(_.code)), currencies, ]; } -export function StoreAddress({ onComplete, isMandatory = false }) { +export function StoreAddress({ onComplete, state, isMandatory = false }) { let [address, setAddress] = useState({}); - let [isLoading, defaultContact, countries, currencies] = useBasicProfile(); + let [defaultCountry, defaultState] = + state?.brandConfig?.defaultContact?.woocommerce_default_country.split(":"); + let defaultContact = { + country: defaultCountry ?? "US", + state: defaultState ?? "AZ", + woocommerce_currency: + state?.brandConfig?.defaultContact?.woocommerce_currency ?? "USD", + woocommerce_store_address: "", + woocommerce_store_address_2: "", + woocommerce_store_city: "", + woocommerce_store_postcode: "", + }; + let [isLoading, countries, currencies] = useBasicProfile(); function handleChange(event) { setAddress({ ...address, [event.target.name]: event.target.value }); } @@ -48,9 +51,10 @@ export function StoreAddress({ onComplete, isMandatory = false }) { await updateWPSettings({ ...defaultContact, ...wcAddress, - woocommerce_default_country: states.length > 0 && selectedState - ? `${selectedCountry}:${selectedState}` - : selectedCountry, + woocommerce_default_country: + states.length > 0 && selectedState + ? `${selectedCountry}:${selectedState}` + : selectedCountry, }); await updateWCOnboarding({ completed: true }); await onComplete(); @@ -140,7 +144,12 @@ export function StoreAddress({ onComplete, isMandatory = false }) { type="text" name="state" required - defaultValue="" + defaultValue={ + selectedCountry === defaultContact.country && + address?.state === undefined + ? defaultContact.state + : "" + } {...eventHandlers} >