diff --git a/assets/css/wc-pre-orders-admin.css b/assets/css/wc-pre-orders-admin.css new file mode 100644 index 0000000..663b277 --- /dev/null +++ b/assets/css/wc-pre-orders-admin.css @@ -0,0 +1 @@ +.widefat .column-order_status mark.pre-ordered{display:block;text-indent:-9999px;position:relative;height:1em;width:1em;background:0 0;font-size:1.4em;margin:0 auto}.widefat .column-order_status mark.pre-ordered:after{font-family:WooCommerce;speak:none;font-weight:400;font-variant:normal;text-transform:none;line-height:1;margin:0;text-indent:0;position:absolute;top:0;left:0;width:100%;height:100%;text-align:center;content:"";color:#7ad03a}.pre-orders .column-status{width:90px}#woocommerce-product-data ul.product_data_tabs li.wc_pre_orders_tab a:before{content:""}.ui-timepicker-div .ui-widget-header{margin-bottom:8px}.ui-timepicker-div dl{text-align:left}.ui-timepicker-div dl dt{height:25px;margin-bottom:-25px}.ui-timepicker-div dl dd{margin:0 10px 10px 65px}.ui-timepicker-div td{font-size:90%}.ui-tpicker-grid-label{background:0 0;border:none;margin:0;padding:0}.ui-timepicker-rtl{direction:rtl}.ui-timepicker-rtl dl{text-align:right}.ui-timepicker-rtl dl dd{margin:0 65px 10px 10px}.widefat.pre-orders td.column-status{padding:6px 7px}.widefat.pre-orders .column-status{text-align:left}.widefat.pre-orders .column-status mark.active,.widefat.pre-orders .column-status mark.cancelled,.widefat.pre-orders .column-status mark.completed{display:block;text-indent:-9999px;position:relative;height:1em;width:1em;background:0 0;font-size:1.4em;margin:0 auto}.widefat.pre-orders .column-status mark.active:after,.widefat.pre-orders .column-status mark.cancelled:after,.widefat.pre-orders .column-status mark.completed:after{font-family:WooCommerce;speak:none;font-weight:400;font-variant:normal;text-transform:none;line-height:1;margin:0;text-indent:0;position:absolute;top:0;left:0;width:100%;height:100%;text-align:center;content:""}.widefat.pre-orders .column-status mark.active:after{content:"\e012";color:#7ad03a}.widefat.pre-orders .column-status mark.completed:after{content:"\e015";color:#2ea2cc}.widefat.pre-orders .column-status mark.cancelled:after{content:"\e013";color:#a00}.widefat.pre-orders .column-customer{width:15%}.widefat.pre-orders .column-product{width:30%}.widefat.pre-orders .column-order{width:15%}.widefat.pre-orders .column-order_date{width:100px}.widefat.pre-orders .column-availability-date{width:100px}.wp-list-table.pre-orders .column-status .row-actions{text-align:center}.wp-list-table.pre-orders .column-status .row-actions .cancel a{color:#a00}.wp-list-table.pre-orders .column-status .row-actions .cancel a:hover{color:#e44} \ No newline at end of file diff --git a/assets/css/wc-pre-orders-admin.scss b/assets/css/wc-pre-orders-admin.scss new file mode 100644 index 0000000..20c3945 --- /dev/null +++ b/assets/css/wc-pre-orders-admin.scss @@ -0,0 +1,187 @@ +// WooCommerce Pre-Orders Admin. + +// Variables from woocommerce/assets/css/_variables.scss. +$green: #7ad03a; +$red: #a00; +$blue: #2ea2cc; + +// Mixins from woocommerce/assets/css/_mixins.scss. +@mixin ir() { + display: block; + text-indent: -9999px; + position: relative; + height: 1em; + width: 1em; +} + +@mixin icon( $glyph: '\e001' ) { + font-family: 'WooCommerce'; + speak: none; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + margin: 0; + text-indent: 0; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + text-align: center; + content: $glyph; +} + +// Order status icon +.widefat .column-order_status { + mark.pre-ordered { + @include ir(); + background: none; + font-size: 1.4em; + margin: 0 auto; + + &:after { + @include icon( "\e012" ); + color: $green; + } + } +} +.pre-orders { + .column-status { + width: 90px; + } +} + +// Product tab +#woocommerce-product-data { + ul.product_data_tabs { + li.wc_pre_orders_tab a { + &:before { + content: ""; + } + } + } +} + +// jQuery UI timepicker +.ui-timepicker-div { + + .ui-widget-header { + margin-bottom: 8px; + } + + dl { + text-align: left; + dt { + height: 25px; margin-bottom: -25px; + } + dd { + margin: 0 10px 10px 65px; + } + } + + td { + font-size: 90%; + } +} + +.ui-tpicker-grid-label { + background: none; + border: none; + margin: 0; + padding: 0; +} + +.ui-timepicker-rtl { + direction: rtl; + dl { + text-align: right; + dd { + margin: 0 65px 10px 10px; + } + } +} + +// List Table +.widefat.pre-orders { + + td { + &.column-status { + padding: 6px 7px; + } + + } + + .column-status { + text-align: left; + + mark.active, mark.completed, mark.cancelled { + @include ir(); + background: none; + font-size: 1.4em; + margin: 0 auto; + + &:after { + @include icon(); + } + } + + mark { + &.active { + &:after { + content: "\e012"; + color: $green; + } + } + &.completed { + &:after { + content: "\e015"; + color: $blue; + } + } + &.cancelled { + &:after { + content: "\e013"; + color: $red; + } + } + } + } + + .column-customer { + width: 15%; + } + + .column-product { + width: 30%; + } + + .column-order { + width: 15%; + } + + .column-order_date { + width: 100px; + } + + .column-availability-date { + width: 100px; + } +} + +// List table row actions +.wp-list-table.pre-orders .column-status .row-actions { + text-align: center; + + .cancel { + a { + color: $red; + + &:hover { + color: $red + #444; + } + } + } +} + + diff --git a/assets/js/admin/wc-pre-orders-admin.js b/assets/js/admin/wc-pre-orders-admin.js new file mode 100644 index 0000000..bf0576e --- /dev/null +++ b/assets/js/admin/wc-pre-orders-admin.js @@ -0,0 +1,46 @@ +jQuery( document ).ready( function( $ ) { + 'use strict'; + + var $actionEmailMessage = $( 'textarea[name="wc_pre_orders_action_email_message"]' ), + $dateTimeField = null; + + // Get the proper datetime field (either on the edit product page or the pre-orders > actions tab + if ( $( 'input[name="_wc_pre_orders_availability_datetime"]' ).length ) { + $dateTimeField = $( 'input[name="_wc_pre_orders_availability_datetime"]' ); + } else if ( $( 'input[name="wc_pre_orders_action_new_availability_date"]').length ) { + $dateTimeField = $( 'input[name="wc_pre_orders_action_new_availability_date"]' ); + } + + // Add Pre-Order DateTimePicker (see http://trentrichardson.com/examples/timepicker/) + if ( null !== $dateTimeField ) { + $dateTimeField.datetimepicker({ + dateFormat: 'yy-mm-dd', + numberOfMonths: 1 + }); + } + + // Hide email notification message textarea when "send email notification" is disabled + if ( $actionEmailMessage.length ) { + $( 'input[name="wc_pre_orders_action_enable_email_notification"]').on( 'change', function() { + if ( ! $( this ).is( ':checked' ) ) { + $actionEmailMessage.removeAttr( 'required' ); + $actionEmailMessage.closest( 'tr' ).hide(); + } else { + $actionEmailMessage.closest( 'tr' ).show(); + $actionEmailMessage.attr( 'required', 'required' ); + } + }).trigger( 'change' ); + } + + /** + * Hide pre-orders options when product type is changed to variable-subscription. + * + * Read explanation about this change in WC_Pre_Orders_Admin_Products::product_data_tab function + * @since 2.0.2 + */ + $( 'body' ).on( 'woocommerce-product-type-change', function ( e, select_val ) { + if ( 'variable-subscription' === select_val ) { + $( 'li.pre_orders_options' ).hide(); + } + } ); +}); diff --git a/assets/js/admin/wc-pre-orders-admin.min.js b/assets/js/admin/wc-pre-orders-admin.min.js new file mode 100644 index 0000000..ae8da20 --- /dev/null +++ b/assets/js/admin/wc-pre-orders-admin.min.js @@ -0,0 +1 @@ +jQuery(document).ready(function(i){"use strict";var e=i('textarea[name="wc_pre_orders_action_email_message"]'),t=null;i('input[name="_wc_pre_orders_availability_datetime"]').length?t=i('input[name="_wc_pre_orders_availability_datetime"]'):i('input[name="wc_pre_orders_action_new_availability_date"]').length&&(t=i('input[name="wc_pre_orders_action_new_availability_date"]')),null!==t&&t.datetimepicker({dateFormat:"yy-mm-dd",numberOfMonths:1}),e.length&&i('input[name="wc_pre_orders_action_enable_email_notification"]').on("change",function(){i(this).is(":checked")?(e.closest("tr").show(),e.attr("required","required")):(e.removeAttr("required"),e.closest("tr").hide())}).trigger("change"),i("body").on("woocommerce-product-type-change",function(e,t){"variable-subscription"===t&&i("li.pre_orders_options").hide()})}); diff --git a/assets/js/jquery-ui-timepicker-addon/jquery-ui-timepicker-addon.js b/assets/js/jquery-ui-timepicker-addon/jquery-ui-timepicker-addon.js new file mode 100644 index 0000000..663195c --- /dev/null +++ b/assets/js/jquery-ui-timepicker-addon/jquery-ui-timepicker-addon.js @@ -0,0 +1,1919 @@ +/* + * jQuery timepicker addon + * By: Trent Richardson [http://trentrichardson.com] + * Version 1.2 + * Last Modified: 02/02/2013 + * + * Copyright 2013 Trent Richardson + * You may use this project under MIT or GPL licenses. + * http://trentrichardson.com/Impromptu/GPL-LICENSE.txt + * http://trentrichardson.com/Impromptu/MIT-LICENSE.txt + */ + +/*jslint evil: true, white: false, undef: false, nomen: false */ + +(function($) { + + /* + * Lets not redefine timepicker, Prevent "Uncaught RangeError: Maximum call stack size exceeded" + */ + $.ui.timepicker = $.ui.timepicker || {}; + if ($.ui.timepicker.version) { + return; + } + + /* + * Extend jQueryUI, get it started with our version number + */ + $.extend($.ui, { + timepicker: { + version: "1.2" + } + }); + + /* + * Timepicker manager. + * Use the singleton instance of this class, $.timepicker, to interact with the time picker. + * Settings for (groups of) time pickers are maintained in an instance object, + * allowing multiple different settings on the same page. + */ + var Timepicker = function() { + this.regional = []; // Available regional settings, indexed by language code + this.regional[''] = { // Default regional settings + currentText: 'Now', + closeText: 'Done', + amNames: ['AM', 'A'], + pmNames: ['PM', 'P'], + timeFormat: 'HH:mm', + timeSuffix: '', + timeOnlyTitle: 'Choose Time', + timeText: 'Time', + hourText: 'Hour', + minuteText: 'Minute', + secondText: 'Second', + millisecText: 'Millisecond', + timezoneText: 'Time Zone', + isRTL: false + }; + this._defaults = { // Global defaults for all the datetime picker instances + showButtonPanel: true, + timeOnly: false, + showHour: true, + showMinute: true, + showSecond: false, + showMillisec: false, + showTimezone: false, + showTime: true, + stepHour: 1, + stepMinute: 1, + stepSecond: 1, + stepMillisec: 1, + hour: 0, + minute: 0, + second: 0, + millisec: 0, + timezone: null, + useLocalTimezone: false, + defaultTimezone: "+0000", + hourMin: 0, + minuteMin: 0, + secondMin: 0, + millisecMin: 0, + hourMax: 23, + minuteMax: 59, + secondMax: 59, + millisecMax: 999, + minDateTime: null, + maxDateTime: null, + onSelect: null, + hourGrid: 0, + minuteGrid: 0, + secondGrid: 0, + millisecGrid: 0, + alwaysSetTime: true, + separator: ' ', + altFieldTimeOnly: true, + altTimeFormat: null, + altSeparator: null, + altTimeSuffix: null, + pickerTimeFormat: null, + pickerTimeSuffix: null, + showTimepicker: true, + timezoneIso8601: false, + timezoneList: null, + addSliderAccess: false, + sliderAccessArgs: null, + controlType: 'slider', + defaultValue: null, + parse: 'strict' + }; + $.extend(this._defaults, this.regional['']); + }; + + $.extend(Timepicker.prototype, { + $input: null, + $altInput: null, + $timeObj: null, + inst: null, + hour_slider: null, + minute_slider: null, + second_slider: null, + millisec_slider: null, + timezone_select: null, + hour: 0, + minute: 0, + second: 0, + millisec: 0, + timezone: null, + defaultTimezone: "+0000", + hourMinOriginal: null, + minuteMinOriginal: null, + secondMinOriginal: null, + millisecMinOriginal: null, + hourMaxOriginal: null, + minuteMaxOriginal: null, + secondMaxOriginal: null, + millisecMaxOriginal: null, + ampm: '', + formattedDate: '', + formattedTime: '', + formattedDateTime: '', + timezoneList: null, + units: ['hour','minute','second','millisec'], + control: null, + + /* + * Override the default settings for all instances of the time picker. + * @param settings object - the new settings to use as defaults (anonymous object) + * @return the manager object + */ + setDefaults: function(settings) { + extendRemove(this._defaults, settings || {}); + return this; + }, + + /* + * Create a new Timepicker instance + */ + _newInst: function($input, o) { + var tp_inst = new Timepicker(), + inlineSettings = {}, + fns = {}, + overrides, i; + + for (var attrName in this._defaults) { + if(this._defaults.hasOwnProperty(attrName)){ + var attrValue = $input.attr('time:' + attrName); + if (attrValue) { + try { + inlineSettings[attrName] = eval(attrValue); + } catch (err) { + inlineSettings[attrName] = attrValue; + } + } + } + } + overrides = { + beforeShow: function (input, dp_inst) { + if (typeof tp_inst._defaults.evnts.beforeShow === 'function'){ + return tp_inst._defaults.evnts.beforeShow.call($input[0], input, dp_inst, tp_inst); + } + }, + onChangeMonthYear: function (year, month, dp_inst) { + // Update the time as well : this prevents the time from disappearing from the $input field. + tp_inst._updateDateTime(dp_inst); + if (typeof tp_inst._defaults.evnts.onChangeMonthYear === 'function') { + tp_inst._defaults.evnts.onChangeMonthYear.call($input[0], year, month, dp_inst, tp_inst); + } + }, + onClose: function (dateText, dp_inst) { + if (tp_inst.timeDefined === true && $input.val() !== '') { + tp_inst._updateDateTime(dp_inst); + } + if (typeof tp_inst._defaults.evnts.onClose === 'function') { + tp_inst._defaults.evnts.onClose.call($input[0], dateText, dp_inst, tp_inst); + } + } + }; + for (i in overrides) { + if (overrides.hasOwnProperty(i)) { + fns[i] = o[i] || null; + } + } + tp_inst._defaults = $.extend({}, this._defaults, inlineSettings, o, overrides, { + evnts:fns, + timepicker: tp_inst // add timepicker as a property of datepicker: $.datepicker._get(dp_inst, 'timepicker'); + }); + tp_inst.amNames = $.map(tp_inst._defaults.amNames, function(val) { + return val.toUpperCase(); + }); + tp_inst.pmNames = $.map(tp_inst._defaults.pmNames, function(val) { + return val.toUpperCase(); + }); + + // controlType is string - key to our this._controls + if(typeof(tp_inst._defaults.controlType) === 'string'){ + if($.fn[tp_inst._defaults.controlType] === undefined){ + tp_inst._defaults.controlType = 'select'; + } + tp_inst.control = tp_inst._controls[tp_inst._defaults.controlType]; + } + // controlType is an object and must implement create, options, value methods + else{ + tp_inst.control = tp_inst._defaults.controlType; + } + + if (tp_inst._defaults.timezoneList === null) { + var timezoneList = ['-1200', '-1100', '-1000', '-0930', '-0900', '-0800', '-0700', '-0600', '-0500', '-0430', '-0400', '-0330', '-0300', '-0200', '-0100', '+0000', + '+0100', '+0200', '+0300', '+0330', '+0400', '+0430', '+0500', '+0530', '+0545', '+0600', '+0630', '+0700', '+0800', '+0845', '+0900', '+0930', + '+1000', '+1030', '+1100', '+1130', '+1200', '+1245', '+1300', '+1400']; + + if (tp_inst._defaults.timezoneIso8601) { + timezoneList = $.map(timezoneList, function(val) { + return val == '+0000' ? 'Z' : (val.substring(0, 3) + ':' + val.substring(3)); + }); + } + tp_inst._defaults.timezoneList = timezoneList; + } + + tp_inst.timezone = tp_inst._defaults.timezone; + tp_inst.hour = tp_inst._defaults.hour < tp_inst._defaults.hourMin? tp_inst._defaults.hourMin : + tp_inst._defaults.hour > tp_inst._defaults.hourMax? tp_inst._defaults.hourMax : tp_inst._defaults.hour; + tp_inst.minute = tp_inst._defaults.minute < tp_inst._defaults.minuteMin? tp_inst._defaults.minuteMin : + tp_inst._defaults.minute > tp_inst._defaults.minuteMax? tp_inst._defaults.minuteMax : tp_inst._defaults.minute; + tp_inst.second = tp_inst._defaults.second < tp_inst._defaults.secondMin? tp_inst._defaults.secondMin : + tp_inst._defaults.second > tp_inst._defaults.secondMax? tp_inst._defaults.secondMax : tp_inst._defaults.second; + tp_inst.millisec = tp_inst._defaults.millisec < tp_inst._defaults.millisecMin? tp_inst._defaults.millisecMin : + tp_inst._defaults.millisec > tp_inst._defaults.millisecMax? tp_inst._defaults.millisecMax : tp_inst._defaults.millisec; + tp_inst.ampm = ''; + tp_inst.$input = $input; + + if (o.altField) { + tp_inst.$altInput = $(o.altField).css({ + cursor: 'pointer' + }).on( 'focus', function() { + $input.trigger("focus"); + }); + } + + if (tp_inst._defaults.minDate === 0 || tp_inst._defaults.minDateTime === 0) { + tp_inst._defaults.minDate = new Date(); + } + if (tp_inst._defaults.maxDate === 0 || tp_inst._defaults.maxDateTime === 0) { + tp_inst._defaults.maxDate = new Date(); + } + + // datepicker needs minDate/maxDate, timepicker needs minDateTime/maxDateTime.. + if (tp_inst._defaults.minDate !== undefined && tp_inst._defaults.minDate instanceof Date) { + tp_inst._defaults.minDateTime = new Date(tp_inst._defaults.minDate.getTime()); + } + if (tp_inst._defaults.minDateTime !== undefined && tp_inst._defaults.minDateTime instanceof Date) { + tp_inst._defaults.minDate = new Date(tp_inst._defaults.minDateTime.getTime()); + } + if (tp_inst._defaults.maxDate !== undefined && tp_inst._defaults.maxDate instanceof Date) { + tp_inst._defaults.maxDateTime = new Date(tp_inst._defaults.maxDate.getTime()); + } + if (tp_inst._defaults.maxDateTime !== undefined && tp_inst._defaults.maxDateTime instanceof Date) { + tp_inst._defaults.maxDate = new Date(tp_inst._defaults.maxDateTime.getTime()); + } + tp_inst.$input.on('focus', function() { + tp_inst._onFocus(); + }); + + return tp_inst; + }, + + /* + * add our sliders to the calendar + */ + _addTimePicker: function(dp_inst) { + var currDT = (this.$altInput && this._defaults.altFieldTimeOnly) ? this.$input.val() + ' ' + this.$altInput.val() : this.$input.val(); + + this.timeDefined = this._parseTime(currDT); + this._limitMinMaxDateTime(dp_inst, false); + this._injectTimePicker(); + }, + + /* + * parse the time string from input value or _setTime + */ + _parseTime: function(timeString, withDate) { + if (!this.inst) { + this.inst = $.datepicker._getInst(this.$input[0]); + } + + if (withDate || !this._defaults.timeOnly) { + var dp_dateFormat = $.datepicker._get(this.inst, 'dateFormat'); + try { + var parseRes = parseDateTimeInternal(dp_dateFormat, this._defaults.timeFormat, timeString, $.datepicker._getFormatConfig(this.inst), this._defaults); + if (!parseRes.timeObj) { + return false; + } + $.extend(this, parseRes.timeObj); + } catch (err) { + $.timepicker.log("Error parsing the date/time string: " + err + + "\ndate/time string = " + timeString + + "\ntimeFormat = " + this._defaults.timeFormat + + "\ndateFormat = " + dp_dateFormat); + return false; + } + return true; + } else { + var timeObj = $.datepicker.parseTime(this._defaults.timeFormat, timeString, this._defaults); + if (!timeObj) { + return false; + } + $.extend(this, timeObj); + return true; + } + }, + + /* + * generate and inject html for timepicker into ui datepicker + */ + _injectTimePicker: function() { + var $dp = this.inst.dpDiv, + o = this.inst.settings, + tp_inst = this, + litem = '', + uitem = '', + max = {}, + gridSize = {}, + size = null; + + // Prevent displaying twice + if ($dp.find("div.ui-timepicker-div").length === 0 && o.showTimepicker) { + var noDisplay = ' style="display:none;"', + html = '
' + tmph + ' | '; + } + } + else{ + for (var m = o[litem+'Min']; m <= max[litem]; m += parseInt(o[litem+'Grid'], 10)) { + gridSize[litem]++; + html += '' + ((m < 10) ? '0' : '') + m + ' | '; + } + } + + html += '
'+p+" | "}else for(var h=i[n+"Min"];h<=r[n];h+=parseInt(i[n+"Grid"],10))l[n]++,u+=''+(h<10?"0":"")+h+" | ";u+="
array('react', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-core-data', 'wp-data', 'wp-element', 'wp-i18n'), 'version' => '69774a8ff160b789e777bfed59e24478');
\ No newline at end of file
diff --git a/build/admin/blocks/date-time-picker/index.js b/build/admin/blocks/date-time-picker/index.js
new file mode 100644
index 0000000..65b9ecf
--- /dev/null
+++ b/build/admin/blocks/date-time-picker/index.js
@@ -0,0 +1 @@
+(function(){var __webpack_modules__={9861:function(e,t,u){"use strict";const n=u(8202),r={findRule(e,t){for(let u=0;u ' . wp_kses_post( $message ) . ' ' . esc_html( $memo['messages'] ) . '
+ ', ' »';
+
+ // Display tabs.
+ foreach ( $this->get_tabs() as $tab_id => $tab_title ) {
+
+ $class = ( $tab_id === $current_tab ) ? 'nav-tab nav-tab-active' : 'nav-tab';
+ $url = add_query_arg( 'tab', $tab_id, admin_url( 'admin.php?page=wc_pre_orders' ) );
+
+ printf( '%s', esc_url( $url ), esc_attr( $class ), esc_attr( $tab_title ) );
+ }
+
+ echo '
';
+
+ // Show any messages.
+ // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+ if ( ! empty( $_GET['success'] ) ) {
+ // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+ switch ( $_GET['success'] ) {
+
+ case 'email':
+ $message = __( 'Pre-order customers emailed successfully', 'woocommerce-pre-orders' );
+ break;
+
+ case 'change-date':
+ $message = __( 'Pre-order date changed', 'woocommerce-pre-orders' );
+ break;
+
+ case 'complete':
+ $message = __( 'Pre-orders completed', 'woocommerce-pre-orders' );
+ break;
+
+ case 'cancel':
+ $message = __( 'Pre-orders cancelled', 'woocommerce-pre-orders' );
+ break;
+
+ default:
+ $message = '';
+ break;
+ }
+
+ if ( $message ) {
+ echo '
';
+ echo '';
+ }
+
+ /**
+ * Show the Pre-Orders > Manage tab content.
+ */
+ private function show_manage_tab() {
+ // Setup 'Manage Pre-Orders' list table and prepare the data.
+ $manage_table = $this->get_pre_orders_list_table();
+ $manage_table->prepare_items();
+
+ echo '';
+ }
+
+ /**
+ * Get the fields to display for the selected action, in the format required by woocommerce_admin_fields().
+ *
+ * @param string $section The current section to get fields for.
+ *
+ * @return array
+ */
+ private function get_action_fields( $section ) {
+
+ $products = array( '' => __( 'Select a product', 'woocommerce-pre-orders' ) );
+
+ foreach ( WC_Pre_Orders_Manager::get_all_pre_order_enabled_products() as $product ) {
+ $products[ $product->get_id() ] = $product->get_formatted_name();
+ }
+
+ $fields = array(
+
+ 'email' => array(
+
+ array(
+ 'name' => __( 'Email pre-order customers', 'woocommerce-pre-orders' ),
+ /* translators: %1$s = Opening anchor tag for WooCommerce email customer note, %2$s = Closing anchor tag */
+ 'desc' => sprintf( __( 'You may send an email message to all customers who have pre-ordered a specific product. This will use the default template specified for the %1$sCustomer Note%2$s Email.', 'wc-pre-orders' ), '', '' ),
+ 'type' => 'title',
+ ),
+
+ array(
+ 'id' => 'wc_pre_orders_action_product',
+ 'name' => __( 'Product', 'woocommerce-pre-orders' ),
+ 'desc_tip' => __( 'Select which product to email all pre-ordered customers.', 'woocommerce-pre-orders' ),
+ 'default' => ' ',
+ 'options' => $products,
+ 'type' => 'select',
+ 'class' => 'wc-enhanced-select',
+ 'custom_attributes' => array(
+ 'required' => 'required',
+ ),
+ ),
+
+ array(
+ 'id' => 'wc_pre_orders_action_email_message',
+ 'name' => __( 'Message', 'woocommerce-pre-orders' ),
+ 'desc_tip' => __( 'Enter a message to include in the email notification to customer. Limited HTML allowed.', 'woocommerce-pre-orders' ),
+ 'css' => 'min-width: 300px;',
+ 'default' => '',
+ 'type' => 'textarea',
+ 'custom_attributes' => array(
+ 'required' => 'required',
+ ),
+ ),
+
+ array( 'type' => 'sectionend' ),
+
+ array(
+ 'name' => __( 'Send emails', 'woocommerce-pre-orders' ),
+ 'type' => 'submit_button',
+ ),
+ ),
+
+ 'change-date' => array(
+
+ array(
+ 'name' => __( 'Change the pre-order release date', 'woocommerce-pre-orders' ),
+ 'desc' => __( 'You may change the release date for all pre-orders of a specific product. This will send an email notification to each customer informing them that the pre-order release date was changed, along with the new release date.', 'woocommerce-pre-orders' ),
+ 'type' => 'title',
+ ),
+
+ array(
+ 'id' => 'wc_pre_orders_action_product',
+ 'name' => __( 'Product', 'woocommerce-pre-orders' ),
+ 'desc_tip' => __( 'Select which product to change the release date for.', 'woocommerce-pre-orders' ),
+ 'default' => ( ! empty( $_GET['action_default_product'] ) ) ? absint( $_GET['action_default_product'] ) : '', // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+ 'options' => $products,
+ 'type' => 'select',
+ 'class' => 'wc-enhanced-select',
+ 'custom_attributes' => array(
+ 'required' => 'required',
+ ),
+ ),
+
+ array(
+ 'id' => 'wc_pre_orders_action_new_availability_date',
+ 'name' => __( 'New availability date', 'woocommerce-pre-orders' ),
+ 'desc_tip' => __( 'The new availability date for the product. This must be later than the current availability date.', 'woocommerce-pre-orders' ),
+ 'default' => '',
+ 'type' => 'text',
+ 'custom_attributes' => array(
+ 'required' => 'required',
+ ),
+ ),
+
+ array(
+ 'id' => 'wc_pre_orders_action_enable_email_notification',
+ 'name' => __( 'Send email notification', 'woocommerce-pre-orders' ),
+ 'desc' => __( 'Uncheck this to prevent email notifications from being sent to customers.', 'woocommerce-pre-orders' ),
+ 'default' => 'yes',
+ 'type' => 'checkbox',
+ ),
+
+ array(
+ 'id' => 'wc_pre_orders_action_email_message',
+ 'name' => __( 'Message', 'woocommerce-pre-orders' ),
+ 'desc_tip' => __( 'Enter a message to include in the email notification to customer.', 'woocommerce-pre-orders' ),
+ 'default' => '',
+ 'css' => 'min-width: 300px;',
+ 'type' => 'textarea',
+ ),
+
+ array( 'type' => 'sectionend' ),
+
+ array(
+ 'name' => __( 'Change release date', 'woocommerce-pre-orders' ),
+ 'type' => 'submit_button',
+ ),
+ ),
+
+ 'complete' => array(
+
+ array(
+ 'name' => __( 'Complete pre-orders', 'woocommerce-pre-orders' ),
+ 'desc' => __( 'You may complete all pre-orders for a specific product. This will charge the customer\'s card the pre-ordered amount, change their order status to completed, and send them an email notification.', 'woocommerce-pre-orders' ),
+ 'type' => 'title',
+ ),
+
+ array(
+ 'id' => 'wc_pre_orders_action_product',
+ 'name' => __( 'Product', 'woocommerce-pre-orders' ),
+ 'desc_tip' => __( 'Select which product to complete all pre-orders for.', 'woocommerce-pre-orders' ),
+ 'default' => ' ',
+ 'options' => $products,
+ 'type' => 'select',
+ 'class' => 'wc-enhanced-select',
+ 'custom_attributes' => array(
+ 'required' => 'required',
+ ),
+ ),
+
+ array(
+ 'id' => 'wc_pre_orders_action_enable_email_notification',
+ 'name' => __( 'Send email notification', 'woocommerce-pre-orders' ),
+ 'desc' => __( 'Uncheck this to prevent email notifications from being sent to customers.', 'woocommerce-pre-orders' ),
+ 'default' => 'yes',
+ 'type' => 'checkbox',
+ ),
+
+ array(
+ 'id' => 'wc_pre_orders_action_email_message',
+ 'name' => __( 'Message', 'woocommerce-pre-orders' ),
+ 'desc_tip' => __( 'Enter a message to include in the email notification to customer.', 'woocommerce-pre-orders' ),
+ 'default' => '',
+ 'css' => 'min-width: 300px;',
+ 'type' => 'textarea',
+ ),
+
+ array( 'type' => 'sectionend' ),
+
+ array(
+ 'name' => __( 'Complete pre-orders', 'woocommerce-pre-orders' ),
+ 'type' => 'submit_button',
+ ),
+ ),
+
+ 'cancel' => array(
+ array(
+ 'name' => __( 'Cancel pre-orders', 'woocommerce-pre-orders' ),
+ 'desc' => __( 'You may cancel all pre-orders for a specific product. This will mark the order as cancelled and send the customer an email notification. If pre-orders were charged upfront, you must manually refund the orders.', 'woocommerce-pre-orders' ),
+ 'type' => 'title',
+ ),
+
+ array(
+ 'id' => 'wc_pre_orders_action_product',
+ 'name' => __( 'Product', 'woocommerce-pre-orders' ),
+ 'desc_tip' => __( 'Select which product to cancel all pre-orders for.', 'woocommerce-pre-orders' ),
+ 'default' => ' ',
+ 'options' => $products,
+ 'type' => 'select',
+ 'class' => 'wc-enhanced-select',
+ 'custom_attributes' => array(
+ 'required' => 'required',
+ ),
+ ),
+
+ array(
+ 'id' => 'wc_pre_orders_action_enable_email_notification',
+ 'name' => __( 'Send email notification', 'woocommerce-pre-orders' ),
+ 'desc' => __( 'Uncheck this to prevent email notifications from being sent to customers.', 'woocommerce-pre-orders' ),
+ 'default' => 'yes',
+ 'type' => 'checkbox',
+ ),
+
+ array(
+ 'id' => 'wc_pre_orders_action_email_message',
+ 'name' => __( 'Message', 'woocommerce-pre-orders' ),
+ 'desc_tip' => __( 'Enter a message to include in the email notification to customer.', 'woocommerce-pre-orders' ),
+ 'default' => '',
+ 'css' => 'min-width: 300px;',
+ 'type' => 'textarea',
+ ),
+
+ array( 'type' => 'sectionend' ),
+
+ array(
+ 'name' => __( 'Cancel pre-orders', 'woocommerce-pre-orders' ),
+ 'type' => 'submit_button',
+ ),
+ ),
+ );
+
+ return ( isset( $fields[ $section ] ) ) ? $fields[ $section ] : array();
+ }
+
+ /**
+ * Generate a submit button, called via a do_action() inside woocommerce_admin_fields() for non-default field types.
+ *
+ * @param array $field The field info.
+ */
+ public function generate_submit_button( $field ) {
+ submit_button( $field['name'] );
+ }
+
+ /**
+ * Save our list option.
+ *
+ * @param string $status unknown.
+ * @param string $option the option name.
+ * @param string $value the option value.
+ *
+ * @return string
+ */
+ public function set_pre_orders_list_option( $status, $option, $value ) {
+ if ( 'wc_pre_orders_edit_pre_orders_per_page' === $option ) {
+ return $value;
+ }
+
+ return $status;
+ }
+
+ /**
+ * Redirect with message notice.
+ *
+ * @since 1.4.7
+ *
+ * @param string $message Message to display
+ */
+ protected function _redirect_with_notice( $message ) {
+ $message_nonce = wp_create_nonce( __FILE__ );
+
+ set_transient( $this->message_transient_prefix . $message_nonce, array( 'messages' => $message ), 60 * 60 );
+
+ // Get our next destination, stripping out all actions and other unneeded parameters.
+ // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+ if ( isset( $_REQUEST['_wp_http_referer'] ) ) {
+ $redirect_url = sanitize_text_field( wp_unslash( $_REQUEST['_wp_http_referer'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+ } elseif ( isset( $_SERVER['REQUEST_URI'] ) ) {
+ $redirect_url = remove_query_arg( array( '_wp_http_referer', '_wpnonce', 'action', 'action2', 'order_id', 'customer_message', 'customer_message2' ), sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) );
+ }
+
+ wp_safe_redirect( esc_url_raw( add_query_arg( 'message', $message_nonce, $redirect_url ) ) );
+ exit;
+ }
+}
+
+new WC_Pre_Orders_Admin_Pre_Orders();
diff --git a/includes/admin/class-wc-pre-orders-admin-products.php b/includes/admin/class-wc-pre-orders-admin-products.php
new file mode 100644
index 0000000..d5d398b
--- /dev/null
+++ b/includes/admin/class-wc-pre-orders-admin-products.php
@@ -0,0 +1,182 @@
+ __( 'Pre-orders', 'woocommerce-pre-orders' ),
+ 'target' => 'wc_pre_orders_data',
+ 'class' => $classes,
+ );
+
+ return $tabs;
+ }
+
+ /**
+ * Add pre-orders options to product writepanel.
+ */
+ public function add_product_tab_options() {
+ include 'views/html-product-tab-options.php';
+ }
+
+ /**
+ * Save pre-order options.
+ *
+ * @param int $post_id The ID of the product being saved.
+ */
+ public function save_product_tab_options( $post_id ) {
+ // phpcs:disable WordPress.Security.NonceVerification.Missing
+ // Don't save any settings if there are active pre-orders.
+ if ( WC_Pre_Orders_Product::product_has_active_pre_orders( $post_id ) ) {
+ return;
+ }
+
+ // pre-orders enabled
+ $is_enabled = isset( $_POST['_wc_pre_orders_enabled'] ) && 'yes' === $_POST['_wc_pre_orders_enabled'];
+ update_post_meta( $post_id, '_wc_pre_orders_enabled', ( $is_enabled ? 'yes' : 'no' ) );
+
+ if ( ! empty( $_POST['_wc_pre_orders_availability_datetime'] ) ) {
+ self::save_availability_date_time(
+ $post_id,
+ sanitize_text_field(
+ wp_unslash(
+ $_POST['_wc_pre_orders_availability_datetime']
+ )
+ )
+ );
+ } else {
+ delete_post_meta( $post_id, '_wc_pre_orders_availability_datetime' );
+ }
+
+ // Pre-order fee.
+ if ( isset( $_POST['_wc_pre_orders_fee'] ) && is_numeric( $_POST['_wc_pre_orders_fee'] ) ) {
+ update_post_meta(
+ $post_id,
+ '_wc_pre_orders_fee',
+ floatval(
+ sanitize_text_field(
+ wp_unslash(
+ $_POST['_wc_pre_orders_fee']
+ )
+ )
+ )
+ );
+ } else {
+ update_post_meta( $post_id, '_wc_pre_orders_fee', '' );
+ }
+
+ // When to charge pre-order amount.
+ if ( isset( $_POST['_wc_pre_orders_when_to_charge'] ) && isset( $_POST['_wc_pre_orders_enabled'] ) && 'yes' === $_POST['_wc_pre_orders_enabled'] ) {
+ update_post_meta( $post_id, '_wc_pre_orders_when_to_charge', ( 'upon_release' === $_POST['_wc_pre_orders_when_to_charge'] ) ? 'upon_release' : 'upfront' );
+ }
+
+ do_action( 'wc_pre_orders_save_product_options', $post_id );
+ }
+
+ /**
+ * Save the availability date/time.
+ *
+ * The date/time a pre-order is released is saved as a unix timestamp adjusted for the site's timezone. For example,
+ * when an admin sets a pre-order to be released on 2013-06-25 12pm EST (UTC-4), it is saved as a timestamp equivalent
+ * to 2013-12-25 4pm UTC. This makes the pre-order release check much easier, as it's a simple timestamp comparison,
+ * because the release datetime and the current time are both in UTC.
+ *
+ * @param int $post_id The ID of the product being saved.
+ * @param string $value The value of the availability date/time.
+ */
+ public static function save_availability_date_time( $post_id, $value ) {
+ try {
+ // Get datetime object from site timezone.
+ $datetime = new DateTime( wp_unslash( $value ), new DateTimeZone( wc_timezone_string() ) );
+
+ // Get the unix timestamp (adjusted for the site's timezone already).
+ $timestamp = $datetime->format( 'U' );
+
+ // Don't allow availability dates in the past.
+ if ( $timestamp <= time() ) {
+ $timestamp = '';
+ }
+
+ // Set the availability datetime.
+ update_post_meta( $post_id, '_wc_pre_orders_availability_datetime', $timestamp );
+
+ } catch ( Exception $e ) {
+ global $wc_pre_orders;
+
+ $wc_pre_orders->log( $e->getMessage() );
+ }
+ }
+}
+
+new WC_Pre_Orders_Admin_Products();
diff --git a/includes/admin/class-wc-pre-orders-admin-settings.php b/includes/admin/class-wc-pre-orders-admin-settings.php
new file mode 100644
index 0000000..8c74948
--- /dev/null
+++ b/includes/admin/class-wc-pre-orders-admin-settings.php
@@ -0,0 +1,202 @@
+settings_tab_id, array( $this, 'show_settings' ) );
+
+ // Save settings.
+ add_action( 'woocommerce_update_options_' . $this->settings_tab_id, array( $this, 'save_settings' ) );
+ }
+
+ /**
+ * Add 'Pre-Orders' tab to WooCommerce Settings tabs
+ *
+ * @param array $settings_tabs Tabs array sans 'Pre-Orders' tab.
+ *
+ * @return array $settings_tabs Now with 100% more 'Pre-Orders' tab!
+ */
+ public function add_settings_tab( $settings_tabs ) {
+ $settings_tabs[ $this->settings_tab_id ] = __( 'Pre-Orders', 'woocommerce-pre-orders' );
+
+ return $settings_tabs;
+ }
+
+ /**
+ * Show the 'Pre-Orders' settings page.
+ */
+ public function show_settings() {
+ woocommerce_admin_fields( $this->get_settings() );
+ }
+
+ /**
+ * Save the 'Pre-Orders' settings page.
+ */
+ public function save_settings() {
+ woocommerce_update_options( $this->get_settings() );
+ }
+
+ /**
+ * Returns settings array for use by output/save functions.
+ *
+ * @return array Settings.
+ */
+ public function get_settings() {
+ return apply_filters(
+ 'wc_pre_orders_settings',
+ array(
+
+ array(
+ 'title' => __( 'Button text', 'woocommerce-pre-orders' ),
+ 'type' => 'title',
+ ),
+
+ array(
+ 'title' => __( 'Add to cart button text', 'woocommerce-pre-orders' ),
+ 'desc' => __( 'This controls the add to cart button text on single product pages for products that have pre-orders enabled.', 'woocommerce-pre-orders' ),
+ 'desc_tip' => true,
+ 'id' => 'wc_pre_orders_add_to_cart_button_text',
+ 'default' => __( 'Pre-order now', 'woocommerce-pre-orders' ),
+ 'type' => 'text',
+ ),
+
+ array(
+ 'title' => __( 'Place Order Button Text', 'woocommerce-pre-orders' ),
+ 'desc' => __( 'This controls the place order button text on the checkout when an order contains a pre-orders.', 'woocommerce-pre-orders' ),
+ 'desc_tip' => true,
+ 'id' => 'wc_pre_orders_place_order_button_text',
+ 'default' => __( 'Place pre-order now', 'woocommerce-pre-orders' ),
+ 'type' => 'text',
+ ),
+
+ array( 'type' => 'sectionend' ),
+
+ array(
+ 'title' => __( 'Product message', 'woocommerce-pre-orders' ),
+ /* translators: %1$s: Availability Time %2$s: Availability Date */
+ 'desc' => sprintf( __( 'Adjust the message by using %1$s{availability_date}%2$s and %1$s{availability_time}%2$s to represent the product\'s availability date and time.', 'woocommerce-pre-orders' ), '', '
' ),
+ 'type' => 'title',
+ ),
+
+ array(
+ 'title' => __( 'Single product page message', 'woocommerce-pre-orders' ),
+ 'desc' => __( 'Add an optional message to the single product page below the price. Use this to announce when the pre-order will be available by using {availability_date} and {availability_time}. Limited HTML is allowed. Leave blank to disable.', 'woocommerce-pre-orders' ),
+ 'desc_tip' => true,
+ 'id' => 'wc_pre_orders_single_product_message',
+ /* translators: %s: Availability Date */
+ 'default' => sprintf( __( 'This item will be released %s.', 'woocommerce-pre-orders' ), '{availability_date}' ),
+ 'type' => 'textarea',
+ ),
+
+ array(
+ 'title' => __( 'Shop loop product message', 'woocommerce-pre-orders' ),
+ 'desc' => __( 'Add an optional message to each pre-order enabled product on the shop loop page below the product title. Use this to announce when the pre-order will be available by using {availability_date} and {availability_time}. Limited HTML is allowed. Leave blank to disable.', 'woocommerce-pre-orders' ),
+ 'desc_tip' => true,
+ 'id' => 'wc_pre_orders_shop_loop_product_message',
+ /* translators: %s: Availability Date */
+ 'default' => sprintf( __( 'Available %s.', 'woocommerce-pre-orders' ), '{availability_date}' ),
+ 'type' => 'textarea',
+ ),
+
+ array( 'type' => 'sectionend' ),
+
+ array(
+ 'title' => __( 'Cart / Checkout display text', 'woocommerce-pre-orders' ),
+ /* translators: %1$s: Order Total %2$s: Availability Date */
+ 'desc' => sprintf( __( 'Adjust the display of the order total by using %1$s{order_total}%2$s to represent the order total and %1$s{availability_date}%2$s to represent the product\'s availability date.', 'woocommerce-pre-orders' ), '', '
' ),
+ 'type' => 'title',
+ ),
+
+ array(
+ 'title' => __( 'Availability date title text', 'woocommerce-pre-orders' ),
+ 'desc' => __( 'This controls the title of the availability date section on the cart/checkout page. Leave blank to disable display of the availability date in the cart.', 'woocommerce-pre-orders' ),
+ 'desc_tip' => true,
+ 'id' => 'wc_pre_orders_availability_date_cart_title_text',
+ 'default' => __( 'Available', 'woocommerce-pre-orders' ),
+ 'type' => 'text',
+ ),
+
+ array(
+ 'title' => __( 'Charged upon release order total format', 'woocommerce-pre-orders' ),
+ 'desc' => __( 'This controls the order total format when the cart contains a pre-order charged upon release. Use this to indicate when the customer will be charged for their pre-order by using {availability_date} and {order_total}.', 'woocommerce-pre-orders' ),
+ 'desc_tip' => true,
+ 'id' => 'wc_pre_orders_upon_release_order_total_format',
+ /* translators: %1$s: Order Total %2$s: Availability Date */
+ 'default' => sprintf( __( '%1$s charged %2$s', 'woocommerce-pre-orders' ), '{order_total}', '{availability_date}' ),
+ 'css' => 'min-width: 300px;',
+ 'type' => 'text',
+ ),
+
+ array(
+ 'title' => __( 'Charged upfront order total format', 'woocommerce-pre-orders' ),
+ 'desc' => __( 'This controls the order total format when the cart contains a pre-order charged upfront. Use this to indicate how the customer is charged for their pre-order by using {availability_date} and {order_total}.', 'woocommerce-pre-orders' ),
+ 'desc_tip' => true,
+ 'id' => 'wc_pre_orders_upfront_order_total_format',
+ /* translators: %s: Order Total */
+ 'default' => sprintf( __( '%s charged upfront', 'woocommerce-pre-orders' ), '{order_total}' ),
+ 'css' => 'min-width: 150px;',
+ 'type' => 'text',
+ ),
+
+ array( 'type' => 'sectionend' ),
+
+ array(
+ 'title' => __( 'Out of stock', 'woocommerce-pre-orders' ),
+ 'type' => 'title',
+ ),
+ array(
+ 'title' => __( 'Enable pre-orders for products that get out of stock', 'woocommerce-pre-orders' ),
+ 'desc' => __( 'When a product becomes out of stock customers will be able to pre-order it. Variable products need to have all variations out of stock.', 'woocommerce-pre-orders' ),
+ 'desc_tip' => true,
+ 'id' => 'wc_pre_orders_auto_pre_order_out_of_stock',
+ 'default' => 'no',
+ 'type' => 'checkbox',
+ ),
+ array( 'type' => 'sectionend' ),
+
+ array(
+ 'title' => __( 'Staging/Test', 'woocommerce-pre-orders' ),
+ 'type' => 'title',
+ ),
+
+ array(
+ 'title' => __( 'Disable automated pre-order processing.', 'woocommerce-pre-orders' ),
+ 'desc' => __( 'This is used for when you\'re on a staging/testing site and don\'t want any pre orders to be processed automatically.', 'woocommerce-pre-orders' ),
+ 'desc_tip' => true,
+ 'id' => 'wc_pre_orders_disable_auto_processing',
+ 'default' => 'no',
+ 'type' => 'checkbox',
+ ),
+ array( 'type' => 'sectionend' ),
+ )
+ );
+ }
+}
+
+new WC_Pre_Orders_Admin_Settings();
diff --git a/includes/admin/class-wc-pre-orders-admin.php b/includes/admin/class-wc-pre-orders-admin.php
new file mode 100644
index 0000000..777defa
--- /dev/null
+++ b/includes/admin/class-wc-pre-orders-admin.php
@@ -0,0 +1,123 @@
+includes();
+ }
+
+ /**
+ * Includes.
+ */
+ protected function includes() {
+ require_once 'class-wc-pre-orders-admin-pre-orders.php';
+ require_once 'class-wc-pre-orders-admin-orders.php';
+ require_once 'class-wc-pre-orders-admin-products.php';
+ require_once 'class-wc-pre-orders-admin-settings.php';
+ }
+
+ /**
+ * Set installed option and default settings / terms.
+ */
+ public function maybe_install() {
+ global $woocommerce;
+
+ $installed_version = get_option( 'wc_pre_orders_version' );
+
+ // Install.
+ if ( ! $installed_version ) {
+
+ $admin_settings = new WC_Pre_Orders_Admin_Settings();
+
+ // Install default settings.
+ foreach ( $admin_settings->get_settings() as $setting ) {
+
+ if ( isset( $setting['default'] ) ) {
+ update_option( $setting['id'], $setting['default'] );
+ }
+ }
+ }
+
+ // Upgrade - installed version lower than plugin version?
+ if ( -1 === version_compare( $installed_version, WC_PRE_ORDERS_VERSION ) ) {
+
+ // New version number.
+ update_option( 'wc_pre_orders_version', WC_PRE_ORDERS_VERSION );
+ }
+ }
+
+ /**
+ * Add Pre-orders screen to woocommerce_screen_ids.
+ *
+ * @param array $ids
+ *
+ * @return array
+ */
+ public function screen_ids( $ids ) {
+ $ids[] = 'woocommerce_page_wc_pre_orders';
+
+ return $ids;
+ }
+
+ /**
+ * Load admin styles & scripts only on needed pages.
+ *
+ * @param string $hook_suffix the menu/page identifier
+ */
+ public function load_styles_scripts( $hook_suffix ) {
+ global $woocommerce, $wc_pre_orders, $wp_scripts;
+
+ // Only load on settings / order / product pages.
+ if ( 'woocommerce_page_wc_pre_orders' === $hook_suffix || 'edit.php' === $hook_suffix || 'post.php' === $hook_suffix || 'post-new.php' === $hook_suffix ) {
+ $suffix = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '' : '.min';
+
+ // Admin CSS
+ wp_enqueue_style( 'wc_pre_orders_admin', $wc_pre_orders->get_plugin_url() . '/assets/css/wc-pre-orders-admin.css', array(), WC_PRE_ORDERS_VERSION );
+
+ // Admin JS
+ wp_enqueue_script( 'wc_pre_orders_admin', $wc_pre_orders->get_plugin_url() . '/assets/js/admin/wc-pre-orders-admin' . $suffix . '.js', WC_PRE_ORDERS_VERSION );
+
+ // Load jQuery UI Date/TimePicker on new/edit product page and pre-orders > actions page
+ if ( 'post.php' === $hook_suffix || 'post-new.php' === $hook_suffix || 'woocommerce_page_wc_pre_orders' === $hook_suffix ) {
+
+ // Get loaded jQuery version
+ $jquery_version = isset( $wp_scripts->registered['jquery-ui-core']->ver ) ? $wp_scripts->registered['jquery-ui-core']->ver : '1.8.2';
+
+ // Load jQuery UI CSS while respecting loaded jQuery version
+ wp_enqueue_style( 'jquery-ui-style', '//ajax.googleapis.com/ajax/libs/jqueryui/' . $jquery_version . '/themes/smoothness/jquery-ui.css' );
+
+ // Load TimePicker add-on which extends jQuery DatePicker
+ wp_enqueue_script( 'jquery_ui_timepicker', $wc_pre_orders->get_plugin_url() . '/assets/js/jquery-ui-timepicker-addon/jquery-ui-timepicker-addon' . $suffix . '.js', array( 'jquery', 'jquery-ui-core', 'jquery-ui-datepicker' ), '1.2' );
+ }
+ }
+
+ }
+}
+
+new WC_Pre_Orders_Admin();
diff --git a/includes/admin/class-wc-pre-orders-product-editor-compatibility.php b/includes/admin/class-wc-pre-orders-product-editor-compatibility.php
new file mode 100644
index 0000000..9fc9d64
--- /dev/null
+++ b/includes/admin/class-wc-pre-orders-product-editor-compatibility.php
@@ -0,0 +1,293 @@
+register_block_type_from_metadata( WC_PRE_ORDERS_PLUGIN_PATH . '/build/admin/blocks/select-control' );
+ BlockRegistry::get_instance()->register_block_type_from_metadata( WC_PRE_ORDERS_PLUGIN_PATH . '/build/admin/blocks/date-time-picker' );
+ BlockRegistry::get_instance()->register_block_type_from_metadata( WC_PRE_ORDERS_PLUGIN_PATH . '/build/admin/blocks/message-control' );
+ }
+ }
+
+ /**
+ * Adds pre-orders meta to the product response.
+ *
+ * @param WP_REST_Response $response The response object.
+ * @param WC_Product $product The product object.
+ *
+ * @return WP_REST_Response
+ */
+ public function add_meta_to_response( $response, $product ) {
+ $data = $response->get_data();
+ $has_orders = WC_Pre_Orders_Product::product_has_active_pre_orders( $product->get_id() );
+ $availability_timestamp = WC_Pre_Orders_Product::get_localized_availability_datetime_timestamp( $product->get_id() );
+ $availability_timestamp = esc_attr( ( 0 === $availability_timestamp ) ? '' : gmdate( 'Y-m-d H:i', $availability_timestamp ) );
+
+ $data['pre_order_enabled'] = get_post_meta( $product->get_id(), '_wc_pre_orders_enabled', true );
+ $data['has_active_orders'] = $has_orders;
+ $data['availability_datetime'] = $availability_timestamp;
+
+ $response->set_data( $data );
+
+ return $response;
+ }
+
+ /**
+ * Saves custom data to product metadata.
+ *
+ * @param WC_Product $product The product object.
+ * @param WP_REST_Request $request The request object.
+ */
+ public function save_custom_data_to_product_metadata( $product, $request ) {
+ $product_id = $product->get_id();
+ $is_enabled = $request->get_param( 'pre_order_enabled' );
+ $datetime = $request->get_param( 'availability_datetime' );
+
+ require_once 'class-wc-pre-orders-admin-products.php';
+
+ if ( ! empty( $datetime ) ) {
+ WC_Pre_Orders_Admin_Products::save_availability_date_time( $product_id, $datetime );
+ } elseif ( ! is_null( $datetime ) ) {
+ delete_post_meta( $product_id, '_wc_pre_orders_availability_datetime' );
+ }
+
+ if ( $is_enabled ) {
+ update_post_meta( $product_id, '_wc_pre_orders_enabled', 'yes' );
+ } elseif ( ! is_null( $is_enabled ) && false === $is_enabled ) {
+ update_post_meta( $product_id, '_wc_pre_orders_enabled', '' );
+ }
+ }
+
+ /**
+ * Adds a Pre-orders section to the product editor under the 'General' group.
+ *
+ * @since 2.1.0
+ *
+ * @param ProductTemplates\Group $variation_group The group instance.
+ */
+ public function add_pre_orders_section( $variation_group ) {
+ $template = $variation_group->get_root_template();
+ $is_simple_product = $this->is_template_valid( $template, 'simple-product' );
+
+ if ( ! $is_simple_product ) {
+ return;
+ }
+
+ /**
+ * Template instance.
+ *
+ * @var ProductFormTemplateInterface $parent
+ */
+ $parent = $variation_group->get_parent();
+ $group = $parent->add_group(
+ array(
+ 'id' => 'woocommerce-pre-orders-group-tab',
+ 'attributes' => array(
+ 'title' => __( 'Pre-orders', 'woocommerce-pre-orders' ),
+ ),
+ )
+ );
+
+ $section = $group->add_section(
+ array(
+ 'id' => 'woo-pre-orders-section',
+ 'attributes' => array(
+ 'title' => __( 'Pre-orders', 'woocommerce-pre-orders' ),
+ ),
+ )
+ );
+
+ $section->add_block(
+ array(
+ 'id' => 'wc_pre_orders_active_pre_orders_message',
+ 'blockName' => 'woocommerce-pre-orders/message-control-block',
+ 'attributes' => array(
+ 'content' => sprintf(
+ /* translators: %s Actions menu page URL */
+ esc_html__(
+ 'There are active pre-orders for this product. To change the release date, use the Actions menu.',
+ 'woocommerce-pre-orders'
+ ),
+ esc_url( admin_url( 'admin.php?page=wc_pre_orders&tab=actions§ion=change-date&action_default_product=postIdPlaceholder' ) )
+ ),
+ ),
+ 'hideConditions' => array(
+ array(
+ 'expression' => '!editedProduct.has_active_orders',
+ ),
+ ),
+ )
+ );
+
+ $section->add_block(
+ array(
+ 'id' => 'wc_pre_orders_change_settings_message',
+ 'blockName' => 'woocommerce-pre-orders/message-control-block',
+ 'attributes' => array(
+ 'content' => sprintf(
+ /* translators: %s Pre orders admin page URL */
+ esc_html__(
+ 'To change other settings, please complete or cancel the active pre-orders first.',
+ 'woocommerce-pre-orders'
+ ),
+ esc_url( admin_url( 'admin.php?page=wc_pre_orders' ) )
+ ),
+ ),
+ 'hideConditions' => array(
+ array(
+ 'expression' => '!editedProduct.has_active_orders',
+ ),
+ ),
+ )
+ );
+
+ $section->add_block(
+ array(
+ 'id' => 'wc_pre_orders_enabled',
+ 'blockName' => 'woocommerce/product-checkbox-field',
+ 'attributes' => array(
+ 'title' => __( 'Enable pre-orders', 'woocommerce-deposits' ),
+ 'label' => sprintf(
+ __( 'Enable pre-orders for this product.', 'woocommerce-deposits' ),
+ ),
+ 'property' => 'pre_order_enabled',
+ 'checkedValue' => 'yes',
+ ),
+ 'disableConditions' => array(
+ array(
+ 'expression' => 'editedProduct.has_active_orders',
+ ),
+ ),
+ )
+ );
+
+ $section->add_block(
+ array(
+ 'id' => 'wc_pre_orders_fee',
+ 'blockName' => 'woocommerce/product-pricing-field',
+ 'attributes' => array(
+ 'property' => 'meta_data._wc_pre_orders_fee',
+ 'label' => __( 'Pre-order Fee', 'woocommerce-pre-orders' ),
+ 'help' => __( 'Set a fee to be charged when a pre-order is placed. Leave blank to not charge a pre-order fee.', 'woocommerce-pre-orders' ),
+ ),
+ 'disableConditions' => array(
+ array(
+ 'expression' => 'editedProduct.has_active_orders',
+ ),
+ ),
+ )
+ );
+
+ $section->add_block(
+ array(
+ 'id' => 'wc_pre_orders_availability_datetime',
+ 'blockName' => 'woocommerce-pre-orders/date-time-picker',
+ 'attributes' => array(
+ 'property' => 'availability_datetime',
+ 'title' => __( 'Availability date/time', 'woocommerce-pre-orders' ),
+ 'help' => __( 'Set the date & time that this pre-order will be available. The product will behave as a normal product when this date/time is reached.', 'woocommerce-pre-orders' ),
+ ),
+ 'disableConditions' => array(
+ array(
+ 'expression' => 'editedProduct.has_active_orders',
+ ),
+ ),
+ )
+ );
+
+ $section->add_block(
+ array(
+ 'id' => 'wc_pre_orders_when_to_charge',
+ 'blockName' => 'woocommerce-pre-orders/select-control-block',
+ 'attributes' => array(
+ 'property' => 'meta_data._wc_pre_orders_when_to_charge',
+ 'title' => __( 'When to Charge', 'woocommerce-pre-orders' ),
+ 'help' => __( 'Select "Upon Release" to charge the entire pre-order amount (the product price + pre-order fee if applicable) when the pre-order becomes available. Select "Upfront" to charge the pre-order amount during the initial checkout.', 'woocommerce-pre-orders' ),
+ 'options' => array(
+ array(
+ 'value' => 'upfront',
+ 'label' => __( 'Upfront', 'woocommerce-pre-orders' ),
+ ),
+ array(
+ 'value' => 'upon_release',
+ 'label' => __( 'Upon Release', 'woocommerce-pre-orders' ),
+ ),
+ ),
+ ),
+ 'disableConditions' => array(
+ array(
+ 'expression' => 'editedProduct.has_active_orders',
+ ),
+ ),
+ )
+ );
+ }
+
+ /**
+ * Returns true if the template is valid.
+ *
+ * @param SimpleProductTemplate|ProductVariationTemplate $template The template object.
+ * @param string $template_id The template ID.
+ *
+ * @return bool
+ */
+ private function is_template_valid( $template, $template_id ) {
+ return $template_id === $template->get_id();
+ }
+}
+
+new WC_Pre_Orders_Product_Editor_Compatibility();
diff --git a/includes/admin/filters.php b/includes/admin/filters.php
new file mode 100644
index 0000000..83863e8
--- /dev/null
+++ b/includes/admin/filters.php
@@ -0,0 +1,56 @@
+key ) {
+ $meta_value = date( 'Y-m-d H:i:s', $meta_value );
+ }
+
+ return $meta_value;
+ },
+ 10,
+ 2
+);
+
+add_filter(
+ 'woocommerce_product_import_process_item_data',
+
+ /**
+ * Should return date in timestamp date format for "_wc_pre_orders_availability_datetime" meta key.
+ *
+ * @since 2.0.0
+ *
+ * @param array $parsed_data Array of csv row data.
+ *
+ * @returns array
+ */
+ function ( $data ) {
+ if ( empty( $data['meta_data'] ) ) {
+ return $data;
+ }
+
+ $meta_keys = wp_list_pluck( $data['meta_data'], 'key' );
+ $index = array_search( '_wc_pre_orders_availability_datetime', $meta_keys, true );
+
+ if ( false !== $index && $data['meta_data'][ $index ]['value'] ) {
+ $data['meta_data'][ $index ] = array_merge(
+ $data['meta_data'][ $index ],
+ array( 'value' => strtotime( $data['meta_data'][ $index ]['value'] ) )
+ );
+ }
+
+ return $data;
+ }
+);
diff --git a/includes/admin/views/html-product-tab-options.php b/includes/admin/views/html-product-tab-options.php
new file mode 100644
index 0000000..605f3be
--- /dev/null
+++ b/includes/admin/views/html-product-tab-options.php
@@ -0,0 +1,87 @@
+
+
+
diff --git a/includes/blocks/class-wc-pre-orders-blocks-gateway.php b/includes/blocks/class-wc-pre-orders-blocks-gateway.php
new file mode 100644
index 0000000..ae8244e
--- /dev/null
+++ b/includes/blocks/class-wc-pre-orders-blocks-gateway.php
@@ -0,0 +1,140 @@
+asset_api = $asset_api;
+ }
+
+ /**
+ * Initializes the payment method type.
+ */
+ public function initialize() {
+
+ $payment_gateways = WC()->payment_gateways->payment_gateways();
+ $is_enabled = isset( $payment_gateways['pre_orders_pay_later'] ) ? $payment_gateways['pre_orders_pay_later']->enabled : 'no';
+
+ $this->enabled = $is_enabled;
+ }
+
+ /**
+ * Returns if this payment method should be active. If false, the scripts will not be enqueued.
+ *
+ * @return boolean
+ */
+ public function is_active() {
+
+ if ( 'yes' !== $this->enabled ) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Returns an array of scripts/handles to be registered for this payment method.
+ *
+ * @return array
+ */
+ public function get_payment_method_script_handles() {
+
+ $base_path = WC_PRE_ORDERS_PLUGIN_PATH;
+ $asset_url = WC_PRE_ORDERS_PLUGIN_URL . '/build/gateway/index.js';
+ $css_url = WC_PRE_ORDERS_PLUGIN_URL . '/build/gateway/index.css';
+ $version = WC_PRE_ORDERS_VERSION;
+ $asset_path = $base_path . '/build/index.asset.php';
+
+ $dependencies = array();
+ if ( file_exists( $asset_path ) ) {
+ $asset = require $asset_path;
+ $version = is_array( $asset ) && isset( $asset['version'] )
+ ? $asset['version']
+ : $version;
+ $dependencies = is_array( $asset ) && isset( $asset['dependencies'] )
+ ? $asset['dependencies']
+ : $dependencies;
+ }
+
+ wp_register_script(
+ 'pre_orders_pay_later',
+ $asset_url,
+ $dependencies,
+ $version,
+ true
+ );
+
+ wp_enqueue_style(
+ 'pre_orders_pay_later_css',
+ $css_url,
+ array(),
+ $version
+ );
+
+ return array( 'pre_orders_pay_later' );
+ }
+
+ /**
+ * Gets payment method supported features.
+ *
+ * @return array
+ */
+ public function get_supported_features() {
+ return array( 'products', 'pre-orders' );
+ }
+
+ /**
+ * Returns an array of key=>value pairs of data made available to the payment methods script.
+ *
+ * @return array
+ */
+ public function get_payment_method_data() {
+
+ return array(
+ 'title' => __( 'Pay later', 'woocommerce-pre-orders' ),
+ 'description' => __( 'You will receive an email when the pre-order is available along with instructions on how to complete your order.', 'woocommerce-pre-orders' ),
+ 'order_button_text' => get_option( 'wc_pre_orders_place_order_button_text', __( 'Place pre-order now', 'woocommerce-pre-orders' ) ),
+ 'supports' => $this->get_supported_features(),
+ 'is_enabled' => WC_Pre_Orders_Blocks_Integration::is_pre_order_and_charged_upon_release(),
+ );
+ }
+}
diff --git a/includes/blocks/class-wc-pre-orders-blocks-integration.php b/includes/blocks/class-wc-pre-orders-blocks-integration.php
new file mode 100644
index 0000000..6d7b44c
--- /dev/null
+++ b/includes/blocks/class-wc-pre-orders-blocks-integration.php
@@ -0,0 +1,119 @@
+includes();
+
+ // Add woocommerce blocks support.
+ $this->add_woocommerce_block_support();
+ }
+
+ /**
+ * Add payment method block support.
+ */
+ public function add_woocommerce_block_support() {
+
+ if ( class_exists( 'Automattic\WooCommerce\Blocks\Payments\Integrations\AbstractPaymentMethodType' ) ) {
+ // Register payment method integrations.
+ add_action( 'woocommerce_blocks_payment_method_type_registration', array( $this, 'register_payment_method_integrations' ) );
+ $this->register_payment_methods();
+ $this->blocks_loaded();
+ }
+ }
+
+ /**
+ * Register payment method.
+ *
+ * @return WC_Pre_Orders_Blocks_Gateway $instance pre-orders gateway instance.
+ */
+ protected function register_payment_methods() {
+ $container = Package::container();
+
+ $container->register(
+ WC_Pre_Orders_Blocks_Gateway::class,
+ function( Container $container ) {
+ $asset_api = $container->get( AssetApi::class );
+ return new WC_Pre_Orders_Blocks_Gateway( $asset_api );
+ }
+ );
+ }
+
+ /**
+ * Register the payment requirements for blocks.
+ *
+ * @return void
+ */
+ public function blocks_loaded() {
+ $args = array(
+ 'data_callback' => array( $this, 'add_pre_order_availability_payment_requirement' ),
+ );
+ if ( function_exists( 'woocommerce_store_api_register_payment_requirements' ) ) {
+ woocommerce_store_api_register_payment_requirements( $args );
+ } else {
+ $extend = Package::container()->get( ExtendRestApi::class );
+ $extend->register_payment_requirements( $args );
+ }
+ WC_Pre_Orders_Extend_Store_API::init();
+ }
+
+ /**
+ * Check if is a pre-order and charged upon release.
+ *
+ * @return bool
+ */
+ public static function is_pre_order_and_charged_upon_release() {
+ return \WC_Pre_Orders_Cart::cart_contains_pre_order() && \WC_Pre_Orders_Product::product_is_charged_upon_release( \WC_Pre_Orders_Cart::get_pre_order_product() );
+ }
+
+ /**
+ * Adds pre_order availability payment requirement for carts that contain a product that requires it.
+ *
+ * @return array
+ */
+ public function add_pre_order_availability_payment_requirement() {
+ if ( $this->is_pre_order_and_charged_upon_release() ) {
+ return array( 'pre-orders' );
+ }
+ return array();
+ }
+
+ /**
+ * Register payment method integration.
+ *
+ * @param PaymentMethodRegistry $payment_method_registry Payment method registry object.
+ */
+ public function register_payment_method_integrations( PaymentMethodRegistry $payment_method_registry ) {
+
+ $payment_method_registry->register(
+ Package::container()->get( WC_Pre_Orders_Blocks_Gateway::class )
+ );
+ }
+
+ /**
+ * Include class that represents the gateway.
+ */
+ public function includes() {
+ require_once __DIR__ . '/class-wc-pre-orders-blocks-gateway.php';
+ }
+}
diff --git a/includes/blocks/class-wc-pre-orders-extend-store-api.php b/includes/blocks/class-wc-pre-orders-extend-store-api.php
new file mode 100644
index 0000000..39df5db
--- /dev/null
+++ b/includes/blocks/class-wc-pre-orders-extend-store-api.php
@@ -0,0 +1,107 @@
+ CartItemSchema::IDENTIFIER,
+ 'namespace' => self::IDENTIFIER,
+ 'data_callback' => array( 'WooCommerce\Pre_Orders\Blocks\WC_Pre_Orders_Extend_Store_API', 'extend_cart_item_data' ),
+ 'schema_callback' => array( 'WooCommerce\Pre_Orders\Blocks\WC_Pre_Orders_Extend_Store_API', 'extend_cart_item_schema' ),
+ 'schema_type' => ARRAY_A,
+ );
+
+ if ( function_exists( 'woocommerce_store_api_register_endpoint_data' ) ) {
+ woocommerce_store_api_register_endpoint_data( $args );
+ } else {
+ $extend = Package::container()->get( ExtendRestApi::class );
+ $extend->register_endpoint_data( $args );
+ }
+ }
+
+ /**
+ * Register pre-order product type data into cart/items endpoint.
+ *
+ * @param array $cart_item Current cart item data.
+ *
+ * @return array $item_data Registered data or empty array if condition is not satisfied.
+ */
+ public static function extend_cart_item_data( $cart_item ) {
+ $product = $cart_item['data'];
+ $item_data = array(
+ 'availability' => null,
+ 'charged_upon_release' => null,
+ 'charged_upfront' => null,
+ );
+
+ if ( \WC_Pre_Orders_Product::product_can_be_pre_ordered( $product->get_id() ) ) {
+ $item_data = array(
+ 'availability' => \WC_Pre_Orders_Product::get_localized_availability_date( $product ),
+ 'charged_upon_release' => \WC_Pre_Orders_Product::product_is_charged_upon_release( $product ),
+ 'charged_upfront' => \WC_Pre_Orders_Product::product_is_charged_upfront( $product ),
+ );
+ }
+
+ return $item_data;
+ }
+
+ /**
+ * Register pre-order product type schema into cart/items endpoint.
+ *
+ * @return array Registered schema.
+ */
+ public static function extend_cart_item_schema() {
+ return array(
+ 'availability' => array(
+ 'description' => __( 'Availability date for product.', 'woocommerce-pre-orders' ),
+ 'type' => array( 'string', 'null' ),
+ 'context' => array( 'view', 'edit' ),
+ 'readonly' => true,
+ ),
+ 'charged_upon_release' => array(
+ 'description' => __( 'Indicates if customer is going to be charged only when product is released.', 'woocommerce-pre-orders' ),
+ 'type' => array( 'boolean', 'null' ),
+ 'context' => array( 'view', 'edit' ),
+ 'readonly' => true,
+ ),
+ 'charged_upfront' => array(
+ 'description' => __( 'Indicates if customer is going to be charged upfront.', 'woocommerce-pre-orders' ),
+ 'type' => array( 'boolean', 'null' ),
+ 'context' => array( 'view', 'edit' ),
+ 'readonly' => true,
+ ),
+ );
+ }
+}
diff --git a/includes/class-wc-pre-orders-cart.php b/includes/class-wc-pre-orders-cart.php
new file mode 100644
index 0000000..0723d91
--- /dev/null
+++ b/includes/class-wc-pre-orders-cart.php
@@ -0,0 +1,296 @@
+cart_contains_pre_order() ) {
+ $total = WC_Pre_Orders_Manager::get_formatted_pre_order_total( $total, self::get_pre_order_product() );
+ }
+
+ return $total;
+ }
+
+
+ /**
+ * Get item data to display on cart/checkout pages that shows the availability date of the pre-order
+ *
+ * @since 1.0
+ * @param array $item_data any existing item data
+ * @param array $cart_item the cart item
+ * @return array
+ */
+ public function get_item_data( $item_data, $cart_item ) {
+
+ // only modify pre-orders on cart/checkout page
+ if ( ! $this->cart_contains_pre_order() ) {
+ return $item_data;
+ }
+
+ // get title text
+ $name = get_option( 'wc_pre_orders_availability_date_cart_title_text' );
+
+ // don't add if empty
+ if ( ! $name ) {
+ return $item_data;
+ }
+
+ $pre_order_meta = apply_filters(
+ 'wc_pre_orders_cart_item_meta',
+ array(
+ 'name' => $name,
+ 'display' => WC_Pre_Orders_Product::get_localized_availability_date( $cart_item['data'] ),
+ ),
+ $cart_item
+ );
+
+ // add title and localized date
+ if ( ! empty( $pre_order_meta ) ) {
+ $item_data[] = $pre_order_meta;
+ }
+
+ return $item_data;
+ }
+
+ /**
+ * Redirect to the cart
+ */
+ public function redirect_to_cart() {
+
+ $data = array(
+ 'error' => true,
+ 'product_url' => wc_get_cart_url(),
+ );
+
+ wp_send_json( $data );
+ }
+
+ /**
+ * When a pre-order is added to the cart, remove any other products
+ *
+ * @since 1.0
+ * @param bool $valid
+ * @param $product_id
+ * @return bool
+ */
+ public function validate_cart( $valid, $product_id ) {
+ global $woocommerce;
+
+ if ( WC_Pre_Orders_Product::product_can_be_pre_ordered( $product_id ) ) {
+
+ // if a pre-order product is being added to cart, check if the cart already contains other products and empty it if it does
+ if ( $woocommerce->cart->get_cart_contents_count() >= 1 ) {
+
+ $woocommerce->cart->empty_cart();
+
+ $string = __( 'Your previous cart was emptied because pre-orders must be purchased separately.', 'woocommerce-pre-orders' );
+
+ // Backwards compatible (pre 2.1) for outputting notice
+ if ( function_exists( 'wc_add_notice' ) ) {
+ wc_add_notice( $string );
+ } else {
+ $woocommerce->add_message( $string );
+ }
+
+ // when adding via ajax, redirect to cart page so that above notices show up
+ add_action( 'woocommerce_ajax_added_to_cart', array( $this, 'redirect_to_cart' ) );
+ }
+
+ // return what was passed in, allowing the pre-order to be added
+ return $valid;
+
+ } else {
+
+ // if there's a pre-order in the cart already, prevent anything else from being added
+ if ( $this->cart_contains_pre_order() ) {
+
+ if ( function_exists( 'wc_add_notice' ) ) {
+ wc_add_notice( __( 'This product cannot be added to your cart because it already contains a pre-order, which must be purchased separately.', 'woocommerce-pre-orders' ), 'error' );
+ } else {
+ // Backwards compatible (pre 2.1) for outputting notice.
+ $woocommerce->add_error( __( 'This product cannot be added to your cart because it already contains a pre-order, which must be purchased separately.', 'woocommerce-pre-orders' ) );
+ }
+
+ $valid = false;
+ }
+ }
+
+ return $valid;
+ }
+
+
+ /**
+ * Add any applicable pre-order fees when calculating totals
+ *
+ * @since 1.0
+ */
+ public function maybe_add_pre_order_fee() {
+ global $woocommerce;
+
+ // Only add pre-order fees if the cart contains a pre-order
+ if ( ! $this->cart_contains_pre_order() ) {
+ return;
+ }
+
+ // Make sure the pre-order fee hasn't already been added
+ if ( $this->cart_contains_pre_order_fee() ) {
+ return;
+ }
+
+ $product = self::get_pre_order_product();
+ $fee = $this->generate_fee( $product );
+
+ if ( null !== $fee ) {
+ $woocommerce->cart->add_fee( $fee['label'], $fee['amount'], $fee['tax_status'] );
+ }
+
+ }
+
+ /**
+ * Generates fee
+ *
+ * @since 1.6.0
+ * @param WC_Product|int $product
+ * @return array|null
+ */
+ public function generate_fee( $product ) {
+
+ // Get pre-order amount
+ $amount = WC_Pre_Orders_Product::get_pre_order_fee( $product );
+
+ if ( 0 >= $amount ) {
+ return;
+ }
+
+ return apply_filters(
+ 'wc_pre_orders_fee',
+ array(
+ 'label' => __( 'Pre-order fee', 'woocommerce-pre-orders' ),
+ 'amount' => $amount,
+ 'tax_status' => WC_Pre_Orders_Product::get_pre_order_fee_tax_status( $product ), // pre order fee inherits tax status of product
+ )
+ );
+ }
+
+ /**
+ * Checks if the current cart contains a product with pre-orders enabled
+ *
+ * @since 1.0
+ * @return bool true if the cart contains a pre-order, false otherwise
+ */
+ public static function cart_contains_pre_order() {
+ global $woocommerce;
+
+ $contains_pre_order = false;
+
+ if ( ! empty( $woocommerce->cart->cart_contents ) ) {
+
+ foreach ( $woocommerce->cart->cart_contents as $cart_item ) {
+ $product_id = ! empty( $cart_item['variation_id'] ) ? $cart_item['variation_id'] : $cart_item['product_id'];
+ if ( WC_Pre_Orders_Product::product_can_be_pre_ordered( $product_id ) ) {
+
+ $contains_pre_order = true;
+ break;
+ }
+ }
+ }
+
+ return $contains_pre_order;
+ }
+
+
+ /**
+ * Checks if the current cart contains a pre-order fee
+ *
+ * @since 1.0
+ * @return bool true if the cart contains a pre-order fee, false otherwise
+ */
+ public static function cart_contains_pre_order_fee() {
+ global $woocommerce;
+
+ foreach ( $woocommerce->cart->get_fees() as $fee ) {
+
+ if ( is_object( $fee ) && 'pre-order-fee' == $fee->id ) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+
+ /**
+ * Since a cart may only contain a single pre-ordered product, this returns the pre-ordered product object or
+ * null if the cart does not contain a pre-order
+ *
+ * @since 1.0
+ * @return object|null the pre-ordered product object, or null if the cart does not contain a pre-order
+ */
+ public static function get_pre_order_product() {
+ global $woocommerce;
+
+ if ( self::cart_contains_pre_order() ) {
+
+ foreach ( $woocommerce->cart->cart_contents as $cart_item ) {
+
+ if ( WC_Pre_Orders_Product::product_can_be_pre_ordered( $cart_item['product_id'] ) ) {
+
+ // return the product object
+ return wc_get_product( $cart_item['product_id'] );
+ }
+ }
+ } else {
+
+ // cart doesn't contain pre-order
+ return null;
+ }
+ }
+
+} // end \WC_Pre_Orders_Cart class
diff --git a/includes/class-wc-pre-orders-checkout.php b/includes/class-wc-pre-orders-checkout.php
new file mode 100644
index 0000000..b4cad50
--- /dev/null
+++ b/includes/class-wc-pre-orders-checkout.php
@@ -0,0 +1,317 @@
+=' ) ) {
+ add_action( 'woocommerce_store_api_checkout_update_order_meta', array( $this, 'add_order_meta_by_order' ) );
+ } else {
+ add_action( 'woocommerce_blocks_checkout_update_order_meta', array( $this, 'add_order_meta_by_order' ) );
+ }
+
+ // change status to pre-ordered when payment is completed for a pre-order charged upfront
+ add_filter( 'woocommerce_payment_complete_order_status', array( $this, 'update_payment_complete_order_status' ), 10, 2 );
+
+ // change status to pre-ordered when payment is completed for a pre-order charged upfront ( for gateways that do not call WC_Order::payment_complete() )
+ add_action( 'woocommerce_order_status_on-hold_to_processing', array( $this, 'update_manual_payment_complete_order_status' ) );
+ add_action( 'woocommerce_order_status_on-hold_to_completed', array( $this, 'update_manual_payment_complete_order_status' ) );
+ add_action( 'woocommerce_order_status_pending_to_processing', array( $this, 'update_manual_payment_complete_order_status' ) );
+
+ // change status to pre-ordered when payment is completed for a pre-order charged upfront that previously failed
+ add_action( 'woocommerce_order_status_failed_to_processing', array( $this, 'update_manual_payment_complete_order_status' ) );
+ add_action( 'woocommerce_order_status_failed_to_completed', array( $this, 'update_manual_payment_complete_order_status' ) );
+
+ // Remove the "is_pre_order" flag when the order is charged upfront and fails.
+ add_action( 'woocommerce_order_status_pending_to_failed', array( $this, 'remove_is_pre_order_on_failure' ) );
+
+ // Filter the thank you page to add release date message.
+ add_filter( 'woocommerce_thankyou_order_received_text', array( $this, 'add_release_date_message' ), 10, 2 );
+ }
+
+ /**
+ * Adds release date message to thank you page.
+ * Only shows for charge upon release.
+ *
+ * @since 1.5.4
+ * @version 1.5.4
+ * @param string $message Original message
+ * @param object $order
+ * @return string
+ */
+ public function add_release_date_message( $message, $order ) {
+ if ( ! WC_Pre_Orders_Order::get_pre_order_product( $order ) ) {
+ return $message;
+ }
+
+ if ( ! WC_Pre_Orders_Order::order_will_be_charged_upon_release( $order ) ) {
+ return $message;
+ }
+
+ $availability_date = WC_Pre_Orders_Product::get_localized_availability_date( WC_Pre_Orders_Order::get_pre_order_product( $order ) );
+
+ /* translators: %s: availability date */
+ $availability_date_text = ( ! empty( $availability_date ) ) ? sprintf( __( ' on %s.', 'woocommerce-pre-orders' ), $availability_date ) : '.';
+
+ /* translators: 1: availability date */
+ $message = sprintf( __( 'Your pre-order has been received. You will be prompted for payment for your order when your pre-order is released%s Your order details are shown below for your reference.', 'woocommerce-pre-orders' ), $availability_date_text ) . "\n\n";
+
+ if ( WC_Pre_Orders_Order::order_has_payment_token( $order ) ) {
+ /* translators: 1: availability date */
+ $message = sprintf( __( 'Your pre-order has been received. You will be automatically charged for your order via your selected payment method when your pre-order is released%s Your order details are shown below for your reference.', 'woocommerce-pre-orders' ), $availability_date_text ) . "\n\n";
+ }
+
+ return $message;
+ }
+
+ /**
+ * Check if is a pre-order and charged upon release
+ *
+ * @return bool
+ */
+ protected function is_pre_order_and_charged_upon_release() {
+ return WC_Pre_Orders_Cart::cart_contains_pre_order() && WC_Pre_Orders_Product::product_is_charged_upon_release( WC_Pre_Orders_Cart::get_pre_order_product() );
+ }
+
+ /**
+ * Conditionally remove any gateways that don't support pre-orders on the checkout page when the pre-order is charged
+ * upon release. This is done because payment info is not required in this case so displaying gateways/payment fields
+ * is not needed.
+ *
+ * @since 1.0
+ *
+ * @param array $available_gateways
+ *
+ * @return array
+ */
+ public function maybe_remove_unsupported_gateways( $available_gateways ) {
+
+ // Backwards compatibility checking for payment page
+ if ( function_exists( 'is_checkout_pay_page' ) ) {
+ $pay_page = is_checkout_pay_page();
+ } else {
+ $pay_page = is_page( wc_get_page_id( 'pay' ) );
+ }
+
+ // On checkout page
+ if (
+ ( $pay_page && $this->is_pre_order_and_charged_upon_release() ) ||
+ ( is_checkout() && $this->is_pre_order_and_charged_upon_release() )
+ ) {
+
+ // Remove any non-supported payment gateways
+ foreach ( $available_gateways as $gateway_id => $gateway ) {
+ if ( ! method_exists( $gateway, 'supports' ) || false === $gateway->supports( 'pre-orders' ) || 'no' == $gateway->enabled ) {
+ unset( $available_gateways[ $gateway_id ] );
+ }
+ }
+ }
+
+ return apply_filters( 'wc_pre_orders_remove_unsupported_gateways', $available_gateways, $this->is_pre_order_and_charged_upon_release() );
+ }
+
+ /**
+ * Modifies the 'Place Order' button text on the checkout page
+ *
+ * @since 1.0
+ * @param string $default_text default place order button text
+ * @return string
+ */
+ public function modify_place_order_button_text( $default_text ) {
+
+ // only modify button text if the cart contains a pre-order
+ if ( ! WC_Pre_Orders_Cart::cart_contains_pre_order() ) {
+ return $default_text;
+ }
+
+ // get custom text if set
+ $text = get_option( 'wc_pre_orders_place_order_button_text' );
+
+ if ( $text ) {
+ return $text;
+ } else {
+ return $default_text;
+ }
+ }
+
+ /**
+ * Adds order meta needed for pre-order functionality,
+ * when coming from an order using WooCommerce Blocks.
+ *
+ * @param WC_Order $order
+ */
+ public function add_order_meta_by_order( $order ) {
+
+ if ( is_a( $order, 'WC_Order' ) ) {
+ $order_id = $order->get_id();
+ $this->add_order_meta( $order_id );
+ }
+ }
+
+ /**
+ * Add order meta needed for pre-order functionality
+ *
+ * @since 1.0
+ * @param int $order_id
+ */
+ public function add_order_meta( $order_id ) {
+
+ // don't add meta to orders that don't contain a pre-order
+ // note the cart is checked here instead of the order since WC_Pre_Orders_Order::order_contains_pre_order() checks the meta that is about to be set here :)
+ if ( ! WC_Pre_Orders_Cart::cart_contains_pre_order() ) {
+ return;
+ }
+
+ // get pre-ordered product
+ $product = WC_Pre_Orders_Cart::get_pre_order_product( $order_id );
+ $order = wc_get_order( $order_id );
+
+ // indicate the order contains a pre-order
+ $order->update_meta_data( '_wc_pre_orders_is_pre_order', 1 );
+
+ // save when the pre-order amount was charged (either upfront or upon release)
+ $order->update_meta_data( '_wc_pre_orders_when_charged', get_post_meta( $product->is_type( 'variation' ) ? $product->get_parent_id() : $product->get_id(), '_wc_pre_orders_when_to_charge', true ) );
+
+ $order->save();
+ }
+
+ /**
+ * Update payment complete order status to pre-ordered for orders that are charged upfront. This handles gateways
+ * that call payment_complete() and prevents an awkward status change from pending->processing->pre-ordered, instead
+ * just showing a nice, clean pending->pre-ordered.
+ *
+ * @since 1.0.0
+ * @version 1.5.1
+ * @param string $new_status the status to change the order to.
+ * @param int $order_id the post ID of the order.
+ * @return string
+ */
+ public function update_payment_complete_order_status( $new_status, $order_id ) {
+
+ $order = wc_get_order( $order_id );
+ if ( ! $order || ! WC_Pre_Orders_Order::order_contains_pre_order( $order ) ) {
+ return $new_status;
+ }
+
+ $zero_cost_order = WC_Pre_Orders_Manager::is_zero_cost_order( $order );
+
+ // Don't change status if pre-order will be charged upon release.
+ if ( WC_Pre_Orders_Order::order_will_be_charged_upon_release( $order ) && ! $zero_cost_order ) {
+ return $new_status;
+ }
+
+ return 'pre-ordered';
+ }
+
+ /**
+ * updates order status to pre-ordered for orders that are charged upfront. This handles gateways that don't call
+ * payment_complete(). Unfortunately status changes show like pending->processing/completed->pre-ordered
+ *
+ * @since 2.0.4 Check whether product can be pre-ordered when processing failed order.
+ * @since 1.9.1 Improve support for "Failed" orders.
+ * @since 1.9.0 Check whether the order item was pre-ordered to process failed pre-ordered order.
+ * @since 1.0
+ * @param int $order_id the post ID of the order
+ * @return string
+ */
+ public function update_manual_payment_complete_order_status( $order_id ) {
+
+ $order = new WC_Order( $order_id );
+ $order_item = WC_Pre_Orders_Order::get_pre_order_item( $order );
+ $product_id = $order_item ? wc_get_product( $order_item['product_id'] ) : null;
+
+ // Failed order does not have "_wc_pre_orders_is_pre_order" meta key.
+ // We remove this meta key to hide failed orders from the "Pre-orders" page.
+ // For this reason, we need to check whether order has a pre-ordered item.
+ $action_hooks = [
+ 'woocommerce_order_status_failed_to_processing',
+ 'woocommerce_order_status_failed_to_completed'
+ ];
+
+ $is_failed_pre_order = in_array( current_action(), $action_hooks ) &&
+ ( WC_Pre_Orders_Order::get_pre_order_item( $order ) instanceof WC_Order_Item ) &&
+ ( $product_id && WC_Pre_Orders_Product::product_can_be_pre_ordered( $product_id ) );
+
+
+ // don't update status for non pre-order orders
+ if ( ! $is_failed_pre_order && ! WC_Pre_Orders_Order::order_contains_pre_order( $order ) ) {
+ return;
+ }
+
+ // don't update if pre-order will be charged upon release
+ if ( WC_Pre_Orders_Order::order_will_be_charged_upon_release( $order ) ) {
+ return;
+ }
+
+ // indicate the order contains a pre-order
+ $order->update_meta_data( '_wc_pre_orders_is_pre_order', 1 );
+
+ // change order status to pre-ordered
+ $order->update_status( 'pre-ordered' );
+
+ // Save order.
+ $order->save();
+ }
+
+ /**
+ * Removes the "_wc_pre_orders_is_pre_order" flag when charged ufront and the payment fails.
+ *
+ * This prevents the pre-order from showing up under the Pre-Orders
+ * table when the payment must be done upfront but fails.
+ *
+ * @since 1.5.31
+ * @param int $order_id the post ID of the order.
+ * @return void
+ */
+ public function remove_is_pre_order_on_failure( $order_id ) {
+ $order = wc_get_order( $order_id );
+
+ // Don't update status for non pre-order orders.
+ if ( ! WC_Pre_Orders_Order::order_contains_pre_order( $order ) ) {
+ return;
+ }
+
+ // Don't update if pre-order will be charged upon release.
+ if ( WC_Pre_Orders_Order::order_will_be_charged_upon_release( $order ) ) {
+ return;
+ }
+
+ // Remove the pre-order flag on failure.
+ $order->delete_meta_data( '_wc_pre_orders_is_pre_order' );
+ $order->save();
+ }
+
+
+} // end \WC_Pre_Orders_Checkout class
diff --git a/includes/class-wc-pre-orders-cron.php b/includes/class-wc-pre-orders-cron.php
new file mode 100644
index 0000000..11ad754
--- /dev/null
+++ b/includes/class-wc-pre-orders-cron.php
@@ -0,0 +1,78 @@
+ $interval,
+ 'display' => sprintf( __( 'Every %d minutes', 'woocommerce-pre-orders' ), $interval / 60 ),
+ );
+
+ return $schedules;
+ }
+
+
+ /**
+ * Add scheduled events to wp-cron if not already added
+ *
+ * @since 1.0
+ * @return array
+ */
+ public function add_scheduled_events() {
+
+ // Schedule pre-order completion check with custom interval named 'wc_pre_orders_completion_check'
+ // note the next execution time if the plugin is deactivated then reactivated is the current time + 5 minutes
+ if ( ! wp_next_scheduled( 'wc_pre_orders_completion_check' ) ) {
+ wp_schedule_event( time() + 300, 'wc_pre_orders_completion_check', 'wc_pre_orders_completion_check' );
+ }
+ }
+
+
+} // end \WC_Pre_Orders_Cron class
diff --git a/includes/class-wc-pre-orders-list-table.php b/includes/class-wc-pre-orders-list-table.php
new file mode 100644
index 0000000..3286bca
--- /dev/null
+++ b/includes/class-wc-pre-orders-list-table.php
@@ -0,0 +1,856 @@
+ __( 'Pre-order', 'woocommerce-pre-orders' ),
+ 'plural' => __( 'Pre-orders', 'woocommerce-pre-orders' ),
+ 'ajax' => false,
+ )
+ );
+ }
+
+ /**
+ * Gets the bulk actions available for pre-orders: complete, cancel
+ * or message.
+ *
+ * @see WP_List_Table::get_bulk_actions()
+ * @since 1.0
+ * @return array associative array of action_slug => action_title
+ */
+ public function get_bulk_actions() {
+
+ $actions = array(
+ 'cancel' => __( 'Cancel', 'woocommerce-pre-orders' ),
+ 'complete' => __( 'Complete', 'woocommerce-pre-orders' ),
+ 'message' => __( 'Customer message', 'woocommerce-pre-orders' ),
+ );
+
+ return $actions;
+ }
+
+ /**
+ * Get list of views available (one per available pre-order status) plus
+ * default of 'all', with counts for each
+ *
+ * @see WP_List_Table::get_views()
+ * @since 1.0
+ * @return array
+ */
+ public function get_views() {
+ global $wpdb;
+
+ if ( ! isset( $this->views ) ) {
+ $this->views = array();
+
+ $_order_meta = $wpdb->postmeta;
+ $_order = $wpdb->posts;
+ $_order_id = 'ID';
+ $_order_meta_id = 'post_id';
+ $_cpt_clause = "AND _order.post_type = 'shop_order' AND _order.post_status != 'trash'";
+
+ if ( WC_Pre_Orders::is_hpos_enabled() ) {
+ $_order_meta = $wpdb->prefix . 'wc_orders_meta';
+ $_order = $wpdb->prefix . 'wc_orders';
+ $_order_id = 'id';
+ $_order_meta_id = 'order_id';
+ $_cpt_clause = '';
+ }
+
+ $query = "
+ SELECT COUNT(_order_meta.meta_value) AS count, _order_meta.meta_value as status
+ FROM {$_order_meta} _order_meta
+ LEFT JOIN {$_order} _order ON (_order.{$_order_id} = _order_meta.{$_order_meta_id})
+ WHERE _order_meta.meta_key = '_wc_pre_orders_status'
+ {$_cpt_clause}
+ AND _order_meta.{$_order_meta_id} IN (
+ SELECT {$_order_meta_id} FROM {$_order_meta}
+ WHERE meta_key = '_wc_pre_orders_is_pre_order'
+ AND CAST(meta_value AS CHAR) = '1'
+ )
+ GROUP BY _order_meta.meta_value
+ ";
+
+ $results = $wpdb->get_results( $query );
+
+ // get the special all/trash counts and organize into status => count
+ $counts = array( 'all' => 0 );
+ $trash_count = 0;
+ foreach ( $results as $row ) {
+ if ( 'trash' === $row->status ) {
+ $trash_count += $row->count;
+ } else {
+ $counts[ $row->status ] = $row->count;
+ $counts['all'] += $row->count;
+ }
+ }
+ $counts['trash'] = $trash_count;
+
+ $base_url = admin_url( 'admin.php?page=wc_pre_orders' );
+
+ // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+ if ( isset( $_REQUEST['s'] ) ) {
+ $search_string = sanitize_text_field( wp_unslash( $_REQUEST['s'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+
+ // When an array is provided, sanitize_text_field returns an empty string.
+ if ( '' !== $search_string ) {
+ $base_url = add_query_arg( 's', rawurlencode( $search_string ), $base_url );
+ }
+ }
+
+ // build the set of views, if any
+ foreach ( $counts as $status => $count ) {
+ if ( $count > 0 ) {
+ if ( $this->get_current_pre_order_status( $counts ) === $status ) {
+ $class = ' class="current"';
+ } else {
+ $class = '';
+ }
+
+ $status_url = add_query_arg( 'pre_order_status', rawurlencode( $status ), $base_url );
+
+ $this->views[ $status ] = sprintf(
+ '%s (%s)',
+ esc_url( $status_url ),
+ $class,
+ ucfirst( $status ),
+ $count
+ );
+ }
+ }
+ }
+
+ return $this->views;
+ }
+
+ /**
+ * Gest the currently selected pre-order status (the current view) if any.
+ * Defaults to 'all'. Status is verified to exist in $available_status if
+ * provided
+ *
+ * @since 1.0
+ * @param array $available_status optional array of status => count used for validation
+ * @return string the current pre-order status
+ */
+ public function get_current_pre_order_status( $available_status = null ) {
+ // is there a status view selected?
+ // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+ $status = isset( $_GET['pre_order_status'] ) ? sanitize_text_field( wp_unslash( $_GET['pre_order_status'] ) ) : 'all';
+
+ // verify the status exists, otherwise default to 'all'
+ if ( ! is_null( $available_status ) && ! isset( $available_status[ $status ] ) ) {
+ return 'all';
+ }
+
+ // otherwise just return the status
+ return $status;
+ }
+
+ /**
+ * Returns the column slugs and titles
+ *
+ * @see WP_List_Table::get_columns()
+ * @since 1.0
+ * @return array of column slug => title
+ */
+ public function get_columns() {
+
+ $columns = array(
+ 'cb' => '',
+ 'status' => '' . __( 'Status', 'woocommerce-pre-orders' ) . '',
+ 'customer' => __( 'Customer', 'woocommerce-pre-orders' ),
+ 'product' => __( 'Product', 'woocommerce-pre-orders' ),
+ 'order' => __( 'Order', 'woocommerce-pre-orders' ),
+ 'order_status' => __( 'Order status', 'woocommerce-pre-orders' ),
+ 'order_date' => __( 'Order date', 'woocommerce-pre-orders' ),
+ 'availability_date' => __( 'Availability date', 'woocommerce-pre-orders' ),
+ );
+
+ return $columns;
+ }
+
+ /**
+ * Returns the sortable columns. We make order_date and order sortable
+ * because they're available right in the posts table, and they make sense
+ * to order over.
+ *
+ * @see WP_List_Table::get_sortable_columns()
+ * @since 1.0
+ * @return array of sortable column slug => array( 'orderby', boolean )
+ * where true indicates the initial sort is descending
+ */
+ public function get_sortable_columns() {
+
+ return array(
+ 'order_date' => array( 'date', false ), // false because the inital sort direction is DESC so we want the first column click to sort ASC
+ 'order' => array( 'ID', false ), // same logic as order_date
+ );
+ }
+
+ /**
+ * Get content for the special checkbox column
+ *
+ * @see WP_List_Table::single_row_columns()
+ * @since 1.0
+ * @param WC_Order $order one row (item) in the table
+ * @return string the checkbox column content
+ */
+ public function column_cb( $order ) {
+ return '';
+ }
+
+ /**
+ * Get column content, this is called once per column, per row item ($order)
+ * returns the content to be rendered within that cell.
+ *
+ * @see WP_List_Table::single_row_columns()
+ * @since 1.0
+ * @param WC_Order $order one row (item) in the table
+ * @param string $column_name the column slug
+ * @return string the column content
+ */
+ public function column_default( $order, $column_name ) {
+ $order_id = $order->get_id();
+
+ switch ( $column_name ) {
+
+ case 'status':
+ $actions = array();
+
+ // determine any available actions
+ if ( WC_Pre_Orders_Manager::can_pre_order_be_changed_to( 'cancelled', $order ) ) {
+ $cancel_url = add_query_arg(
+ array(
+ 'order_id' => $order_id,
+ 'action' => 'cancel_pre_order',
+ )
+ );
+ $cancel_url = wp_nonce_url( $cancel_url, 'cancel_pre_order', 'cancel_pre_order_nonce' );
+
+ $actions['cancel'] = sprintf( '%s', esc_url( $cancel_url ), esc_html__( 'Cancel', 'woocommerce-pre-orders' ) );
+ }
+
+ $column_content = sprintf( '%s', WC_Pre_Orders_Order::get_pre_order_status( $order ), wc_sanitize_tooltip( WC_Pre_Orders_Order::get_pre_order_status_to_display( $order ) ), WC_Pre_Orders_Order::get_pre_order_status_to_display( $order ) );
+ $column_content .= $this->row_actions( $actions );
+ break;
+
+ case 'customer':
+ $billing_email = $order->get_billing_email();
+ $user_id = $order->get_customer_id();
+
+ if ( 0 !== $user_id ) {
+ $column_content = sprintf( '%s', get_edit_user_link( $user_id ), $billing_email );
+ } else {
+ $column_content = $billing_email;
+ }
+
+ break;
+
+ case 'product':
+ // Past pre-orders may contain products that are no longer marked as a pre-order product
+ // As only one product can exist in a pre-order, pick the first product
+ $items = $order->get_items();
+ $item = reset( $items );
+
+ if ( $item ) {
+ $product_edit = get_edit_post_link( $item['product_id'] );
+ $column_content = ( $product_edit ) ? sprintf( '%s', $product_edit, $item['name'] ) : $item['name'];
+ } else {
+ $column_content = '';
+ }
+ break;
+
+ case 'order':
+ /* translators: %s: order number */
+ $column_content = sprintf( '%s', $order->get_edit_order_url(), sprintf( __( 'Order %s', 'woocommerce-pre-orders' ), $order->get_order_number() ) );
+ break;
+
+ case 'order_date':
+ $column_content = date_i18n( wc_date_format(), strtotime( ( $order->get_date_created() ? gmdate( 'Y-m-d H:i:s', $order->get_date_created()->getOffsetTimestamp() ) : '' ) ) );
+ break;
+
+ case 'order_status':
+ $column_content = sprintf( '%s', esc_attr( sanitize_html_class( 'status-' . $order->get_status() ) ), esc_html( wc_get_order_status_name( $order->get_status() ) ) );
+ break;
+
+ case 'availability_date':
+ $product = WC_Pre_Orders_Order::get_pre_order_product( $order );
+ $column_content = WC_Pre_Orders_Product::get_localized_availability_date( $product, '--' );
+ break;
+
+ default:
+ $column_content = '';
+ break;
+ }
+
+ return $column_content;
+ }
+
+ /**
+ * Output any messages from the bulk action handling
+ *
+ * @since 1.0
+ */
+ public function render_messages() {
+ // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+ if ( isset( $_GET['message'] ) ) {
+
+ $memo = get_transient( $this->message_transient_prefix . wp_kses_post( wp_unslash( $_GET['message'] ) ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+
+ if ( ! empty( $memo ) ) {
+
+ delete_transient( $this->message_transient_prefix . wp_kses_post( wp_unslash( $_GET['message'] ) ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+
+ if ( ! empty( $memo['messages'] ) ) {
+ echo '
+ ', ' »' ); + ?> +
+ '; + + $user_string = ''; + $user_id = ''; + if ( ! empty( $_GET['_customer_user'] ) ) { + $user_id = absint( $_GET['_customer_user'] ); + $user = get_user_by( 'id', $user_id ); + $user_string = esc_html( $user->display_name ) . ' (#' . absint( $user->ID ) . ' – ' . esc_html( $user->user_email ); + } + + $product_name = ''; + $product_id = ''; + if ( ! empty( $_GET['_product'] ) ) { + $product_id = absint( $_GET['_product'] ); + $product = wc_get_product( $product_id ); + $product_name = ! empty( $product ) ? $product->get_formatted_name() : ''; + } + ?> + + + + + render_availability_dates_dropdown(); + + submit_button( esc_html__( 'Filter', 'woocommerce-pre-orders' ), 'button', false, false, array( 'id' => 'post-query-submit' ) ); + echo ''; + + // Bulk action fields + echo ' '; + + $javascript = " + $( 'select[name=\"action\"]' ).on( 'change', function() { + if ( -1 == $(this).val() ) { + $( '#bulk-action-fields' ).slideUp(); + } else { + $( '#bulk-action-fields' ).slideDown(); + } + }).trigger( 'change' ); + + $( 'select[name=\"action2\"]' ).on( 'change', function() { + if ( -1 == $(this).val() ) { + $('#bulk-action-fields2').slideUp(); + } else { + $('#bulk-action-fields2').slideDown(); + } + }).trigger( 'change' ); + + $( 'span.cancel' ).on( 'click', function( e ) { + if ( ! window.confirm( '" . __( 'Are you sure you want to cancel this pre-order?', 'woocommerce-pre-orders' ) . "' ) ) { + e.preventDefault(); + } + }); + "; + + if ( function_exists( 'wc_enqueue_js' ) ) { + wc_enqueue_js( $javascript ); + } else { + $woocommerce->add_inline_js( $javascript ); + } + } elseif ( 'bottom' === $which ) { + // Bulk action fields + echo ' '; + } + } + + /** + * Display a monthly dropdown for filtering items by availability date + * + * @since 1.0 + */ + private function render_availability_dates_dropdown() { + global $wpdb, $wp_locale; + + // Performance: we could always pull out the database order-by and sort in code to get rid of a 'filesort' from the query + $months = $wpdb->get_results( + " + SELECT DISTINCT YEAR( FROM_UNIXTIME( meta_value ) ) AS year, MONTH( FROM_UNIXTIME( meta_value ) ) AS month + FROM {$wpdb->postmeta} + WHERE meta_key = '_wc_pre_orders_availability_datetime' + AND meta_value > 0 + ORDER BY meta_value+0 DESC + " + ); + + $month_count = count( $months ); + + if ( ! $month_count || ( 1 === $month_count && 0 === $months[0]->month ) ) { + return; + } + + $availability_date = isset( $_GET['availability_date'] ) ? (int) $_GET['availability_date'] : 0; + ?> + + has_scheduled_batch_processor( $product_ids, $batch_id ) ) { + $this->schedule_batch_processor( $product_ids, $batch_id ); + + // Now that we have scheduled the action to process these products. Disable further pre-orders for them. + $this->disable_pre_orders_for_products( $product_ids ); + } + } + + /** + * Schedules a batch processor for a given batch of product IDs. + * + * @param int[] $product_ids The product IDs to batch process pre-orders for. + * @param string $product_ids The unique batch ID for this set of products. + */ + private function schedule_batch_processor( $product_ids, $batch_id ) { + // Schedule the batch to run as soon as possible. WC_Action_Queue::add() uses time(). + WC()->queue()->add( + self::$scheduled_batch_processing_hook, + array( + 'products' => $product_ids, + 'batch_id' => $batch_id, + ) + ); + } + + /** + * Checks if there's already a scheduled batch processor action for a given set of products. + * + * @param int[] $product_ids The product IDs to batch process pre-orders for. + * @param string $product_ids The unique batch ID for this set of products. + * + * @return bool True if there's a scheduled action already, otherwise false. + */ + private function has_scheduled_batch_processor( $product_ids, $batch_id ) { + return null !== WC()->queue()->get_next( + self::$scheduled_batch_processing_hook, + array( + 'products' => $product_ids, + 'batch_id' => $batch_id, + ) + ); + } + + /** + * Schedules a background job to process a single pre-order. + * + * @param WC_Order $order The order the schedule a completetion hook for. + */ + private function schedule_pre_order_complete( $order ) { + $args = array( 'order_id' => $order->get_id() ); + + if ( null === WC()->queue()->get_next( self::$scheduled_pre_order_complete_hook, $args ) ) { + WC()->queue()->schedule_single( time() + MINUTE_IN_SECONDS, self::$scheduled_pre_order_complete_hook, $args ); + } + } + + /** + * Finds pre-orders for a batch of given products and schedules a background job to process them. + * + * @since 2.0.0 + * + * @param int[] $product_ids The product IDs to schedule pre-order completion for. + * @param string $product_ids The unique batch ID for this set of products. + */ + public function schedule_actions_to_complete_pre_orders( $product_ids, $batch_id ) { + // Generate a meta key flag which is stored on orders we have checked if they need processing by this batch so we can exclude them from future queries. + $meta_key_flag = "_wc_pre_handled_by_product_batch_{$batch_id}"; + $batch_size = apply_filters( 'wc_pre_orders_complete_pre_orders_batch_size', 200 ); + + // Get pre-orders which haven't been handled by this batch ID. + $args = array( + 'post_status' => 'wc-pre-ordered', + 'post_type' => 'shop_order', + 'posts_per_page' => $batch_size, + 'fields' => 'ids', + 'meta_query' => array( + 'relation' => 'AND', + array( + 'key' => '_wc_pre_orders_is_pre_order', + 'value' => 1, + ), + array( + 'key' => $meta_key_flag, + 'compare' => 'NOT EXISTS', + ), + ), + ); + + $results = array(); + if( WC_Pre_Orders::is_hpos_enabled() ) { + $results = wc_get_orders( $args ); + } else { + $query = new WP_Query( $args ); + $results = $query->posts; + } + + // If we got a full batch of orders, we haven't finished. + $is_batch_complete = count( $results ) !== $batch_size; + + // If we've finished processing these products, release the orders and clean up the meta flags. + if ( $is_batch_complete ) { + $this->release_orders_from_batch( $meta_key_flag ); + } else { + // If we got a full batch of orders, schedule a followup action. + $this->schedule_batch_processor( $product_ids, $batch_id ); + } + + foreach ( $results as $order_id ) { + $order = wc_get_order( $order_id ); + + if ( ! $order ) { + continue; + } + + // Store meta on the order so we know it has been checked by this batch so it can be excluded by future batch queries. + // Skip setting this meta if this is the last batch, as we've deleted it from all previous orders at this point. + if ( ! $is_batch_complete ) { + $order->update_meta_data( $meta_key_flag, 'true' ); + $order->save(); + } + + foreach ( $order->get_items() as $item ) { + // If this order contains a product we need to process. Schedule an action to process it in the future. + if ( in_array( $item->get_product_id(), $product_ids ) || in_array( $item->get_variation_id(), $product_ids ) ) { + $this->schedule_pre_order_complete( $order ); + continue; + } + } + } + } + + /** + * Deletes a batch ID flag stored on orders handled by a batch processor. + * + * @param string $meta_key_flag The unique meta key used to flag orders as being handled by a batch processor. + * @return int The number of orders updated. + */ + private function release_orders_from_batch( $meta_key_flag ) { + global $wpdb; + if ( WC_Pre_Orders::is_hpos_enabled() ) { + return $wpdb->delete( "{$wpdb->prefix}wc_orders_meta", array( 'meta_key' => $meta_key_flag ) ); + } + return $wpdb->delete( "{$wpdb->prefix}postmeta", array( 'meta_key' => $meta_key_flag ) ); + } + + /** + * Prevent completed pre-orders from being cancelled - Any new pre-orders that have not been processed yet + * (e.g. by checking out via PayPal but not completing purchase) should respect the default order cancel settings + * + * @since 1.0 + * @param bool $cancel_order whether to cancel the pending order or not + * @param object $order the \WC_Order object + * @return bool true if the order should be cancelled, false otherwise + */ + public function maybe_prevent_pending_order_cancel( $cancel_order, $order ) { + if ( WC_Pre_Orders_Order::order_contains_pre_order( $order ) && 'completed' === WC_Pre_Orders_Order::get_pre_order_status( $order ) ) { + $cancel_order = false; + } + + return $cancel_order; + } + + /** + * Prevent order stock reduction when WC_Order::payment_complete() is called for a pre-order charged upon release. + * Because order stock for pre-orders charged upon release is reduced during initial checkout, this prevents stock from + * being reduced twice. + * + * @since 1.0 + * @param bool $reduce_stock whether to reduce stock for the order or not + * @param int $order_id the post ID of the order + * @return bool true if the order stock should be reduced, false otherwise + */ + public function maybe_prevent_payment_complete_order_stock_reduction( $reduce_stock, $order_id ) { + + $order = new WC_Order( $order_id ); + + $order_status = $order->get_status(); + + // stock reduction should only be prevented when order is being completed + // ie when current order status is 'processing' + if ( 'processing' !== $order_status ) { + return $reduce_stock; + } + + if ( WC_Pre_Orders_Order::order_contains_pre_order( $order ) && WC_Pre_Orders_Order::order_will_be_charged_upon_release( $order ) && ! self::is_order_pay_later( $order_id ) ) { + $reduce_stock = false; + } + + return $reduce_stock; + } + + /** + * Reduce the stock level for an order and record the stock reduction in data store + * + * @since 1.5.31 + * @param object $order the \WC_Order object + */ + public static function reduce_stock_level( $order ) { + $order_id = $order->get_id(); + + wc_reduce_stock_levels( $order->get_id() ); + $order->get_data_store()->set_stock_reduced( $order_id, true ); + } + + /** + * Gets all pre-orders + * + * @since 1.0 + * @return array + */ + public static function get_all_pre_orders() { + + $args = array( + 'post_type' => 'shop_order', + 'post_status' => array_keys( wc_get_order_statuses() ), + 'posts_per_page' => -1, + 'meta_key' => '_wc_pre_orders_is_pre_order', + 'meta_value' => 1, + ); + + $results = array(); + if ( WC_Pre_Orders::is_hpos_enabled() ) { + $results = wc_get_orders( $args ); + } else { + $query = new WP_Query( $args ); + if ( empty( $query->posts ) ) { + return array(); + } + $results = $query->posts; + } + + $orders = array(); + + foreach ( $results as $order_post ) { + $order = new WC_Order( $order_post ); + $orders[] = $order; + } + + return $orders; + } + + + /** + * Gets all pre-orders for a given product + * + * @since 1.0 + * @param object|int $product + * @return array + */ + public static function get_all_pre_orders_by_product( $product ) { + global $wpdb; + + if ( ! is_object( $product ) ) { + $product = wc_get_product( $product ); + + if ( ! is_object( $product ) ) { + return array(); + } + } + + if ( WC_Pre_Orders::is_hpos_enabled() ) { + $order_ids = $wpdb->get_results( + $wpdb->prepare( + " + SELECT items.order_id AS id + FROM {$wpdb->prefix}woocommerce_order_items AS items + LEFT JOIN {$wpdb->prefix}woocommerce_order_itemmeta AS item_meta ON items.order_item_id = item_meta.order_item_id + LEFT JOIN {$wpdb->prefix}wc_orders_meta AS order_meta ON items.order_id = order_meta.order_id + WHERE + items.order_item_type = 'line_item' AND + item_meta.meta_key = '_product_id' AND + item_meta.meta_value = %s AND + order_meta.meta_key = '_wc_pre_orders_is_pre_order' AND + order_meta.meta_value = '1' + ", + $product->get_id() + ) + ); + } else { + $order_ids = $wpdb->get_results( + $wpdb->prepare( + " + SELECT items.order_id AS id + FROM {$wpdb->prefix}woocommerce_order_items AS items + LEFT JOIN {$wpdb->prefix}woocommerce_order_itemmeta AS item_meta ON items.order_item_id = item_meta.order_item_id + LEFT JOIN {$wpdb->postmeta} AS post_meta ON items.order_id = post_meta.post_id + WHERE + items.order_item_type = 'line_item' AND + item_meta.meta_key = '_product_id' AND + item_meta.meta_value = %s AND + post_meta.meta_key = '_wc_pre_orders_is_pre_order' AND + post_meta.meta_value = '1' + ", + $product->get_id() + ) + ); + } + + if ( empty( $order_ids ) ) { + return array(); + } + + $orders = array(); + + foreach ( $order_ids as $order ) { + $orders[] = new WC_Order( $order->id ); + } + + return $orders; + } + + /** + * Gets all pre-orders for the currently logged in user, or the user identified by $user_id + * + * @since 1.0 + * @param int $user_id optional user id to return pre-orders for. Defaults to the currently logged in user. + * @return array of WC_Order objects + */ + public static function get_users_pre_orders( $user_id = null ) { + + if ( is_null( $user_id ) ) { + $user_id = get_current_user_id(); + } + + $args = array( + 'customer_id' => $user_id, + 'meta_key' => '_wc_pre_orders_is_pre_order', + 'meta_value' => 1, + 'meta_compare' => '=', + 'return' => 'ids', + ); + + $results = wc_get_orders( $args ); + $orders = array(); + + foreach ( $results as $order_post ) { + $orders[] = new WC_Order( $order_post ); + } + + return apply_filters( 'wc_pre_orders_users_pre_orders', $orders, $user_id ); + } + + /** + * Returns true if the pre-order identified by $order/$item can be changed to + * $new_status + * + * @since 1.0 + * @param string $new_status the new status + * @param WC_Order $order the order object + * @return boolean true if the status can be changed, false otherwise + */ + public static function can_pre_order_be_changed_to( $new_status, $order ) { + + $status = WC_Pre_Orders_Order::get_pre_order_status( $order ); + + // assume it can not be changed + $can_be_changed = false; + + switch ( $new_status ) { + case 'cancelled': + if ( ! in_array( $status, array( 'cancelled', 'completed', 'trash' ) ) ) { + $can_be_changed = true; + } + break; + case 'completed': + if ( 'active' == $status ) { + $can_be_changed = true; + } + break; + } + + return apply_filters( 'wc_pre_orders_status_can_be_changed_to_' . $new_status, $can_be_changed, $order ); + } + + + /** + * Return a link for customers to change the status of their pre-order to $status + * + * @since 1.0 + * @param string $new_status the new status + * @param WC_Order $order the order object + * @return string + */ + public static function get_users_change_status_link( $new_status, $order ) { + $order_id = $order->get_id(); + $action_link = add_query_arg( + array( + 'order_id' => $order_id, + 'status' => $new_status, + ) + ); + $action_link = wp_nonce_url( $action_link, $order_id ); + + return apply_filters( 'wc_pre_orders_users_action_link', $action_link, $order, $new_status ); + } + + + /** + * Gets all products that are currently pre-order enabled + * + * @since 1.9.0 Return private and published pre-order products. + * @since 1.0 + * @return array of WC_Product objects + */ + public static function get_all_pre_order_enabled_products() { + $args = array( + 'fields' => 'ids', + 'post_type' => 'product', + 'post_status' => array( 'publish', 'private' ), + 'nopaging' => true, + 'meta_key' => '_wc_pre_orders_enabled', + 'meta_value' => 'yes', + ); + + $product_post_ids = get_posts( $args ); + + $products = array(); + + foreach ( $product_post_ids as $product_id ) { + + $products[] = wc_get_product( $product_id ); + } + + return $products; + } + + + /** + * Sends a notification email (using the built-in 'Customer Note' email + * template) to all customers associated with the supplied $orders with an active pre-order + * + * @since 1.0 + * @param int|WC_Order $order order object or identifier + * @param string $message required message to include in notification email to customer + */ + public static function email_pre_order_customer( $order, $message ) { + global $woocommerce; + + // load email classes + $woocommerce->mailer(); + + if ( ! is_object( $order ) ) { + $order = new WC_Order( $order ); + } + + if ( 'active' !== WC_Pre_Orders_Order::get_pre_order_status( $order ) ) { + return; + } + + // set email args + $args = array( + 'order_id' => $order->get_id(), + 'customer_note' => $message, + ); + + // fire the notification, which sends the emails + do_action( 'woocommerce_new_customer_note_notification', $args ); + } + + + /** + * Sends a notification email (using the built-in 'Customer Note' email + * template) to all customers associated with the supplied $orders with an active pre-order + * + * @since 1.0 + * @param array $orders array of post ID or WC_Order objects + * @param string $message required message to include in notification email to customer + */ + public static function email_pre_order_customers( $orders, $message ) { + foreach ( $orders as $order ) { + self::email_pre_order_customer( $order, $message ); + } + } + + /** + * Sends a notification email (using the built-in 'Customer Note' email template) to all customers who have pre-ordered + * the given product + * + * @since 1.0 + * @param object|int $product the product to email all pre-ordered customers for + * @param string $message required message to include in notification email to customer + */ + public static function email_all_pre_order_customers( $product, $message ) { + + $orders = self::get_all_pre_orders_by_product( $product ); + + if ( empty( $orders ) ) { + return; + } + + self::email_pre_order_customers( $orders, $message ); + } + + /** + * Change the release date for pre-orders by updating the availability date for the pre-ordered product to a new date in the future + * + * @since 1.0 + * @param object|int $product the product to change the release date for all pre-orders for + * @param string $new_availability_date the new availability date + * @param string $message an optional message to include in communications to the customer + */ + public static function change_release_date_for_all_pre_orders( $product, $new_availability_date, $message = '' ) { + + if ( ! is_object( $product ) ) { + $product = wc_get_product( $product ); + + if ( ! is_object( $product ) ) { + return; + } + } + + // get new availability date timestamp + try { + + // get datetime object from site timezone + $datetime = new DateTime( $new_availability_date, new DateTimeZone( wc_timezone_string() ) ); + + // get the unix timestamp (adjusted for the site's timezone already) + $timestamp = $datetime->format( 'U' ); + + // Whether the product's release date passed. + $is_past_date = $timestamp <= time(); + + } catch ( Exception $e ) { + global $wc_pre_orders; + $wc_pre_orders->log( $e->getMessage() ); + $timestamp = ''; + } + + // set new availability date for product + update_post_meta( $product->get_id(), '_wc_pre_orders_availability_datetime', $timestamp ); + + // get associated orders + $orders = self::get_all_pre_orders_by_product( $product ); + + // fire action for each order + foreach ( $orders as $order ) { + + if ( ! is_object( $order ) ) { + $order = new WC_Order( $order ); + } + + // only delay active pre-orders + if ( 'active' !== WC_Pre_Orders_Order::get_pre_order_status( $order ) ) { + continue; + } + + // When the product's release date passed and the order + // status is set to 'pre-ordered', mark pre-orders as complete. + if ( $is_past_date && 'pre-ordered' === $order->get_status() ) { + // Doing this now to not to wait for the 'wc_pre_orders_completion_check' cron job. + self::complete_pre_order( $order ); + } + + // Add 'release date changed' order note for admins. + /* translators: %s: Availability date */ + $order->add_order_note( sprintf( __( 'Pre-order release date changed to %s', 'woocommerce-pre-orders' ), WC_Pre_Orders_Product::get_localized_availability_date( $product, __( 'N/A', 'woocommerce-pre-orders' ) ) ) ); + + do_action( + 'wc_pre_orders_pre_order_date_changed', + array( + 'order' => $order, + 'availability_date' => $new_availability_date, + 'message' => $message, + ) + ); + } + + // Doing this after the pre_order_date_changed emails are sent + // so the availability date in the email isn't "at a future date". + if ( $is_past_date ) { + // Unmark the product as a pre-order now that it's released. + update_post_meta( $product->get_id(), '_wc_pre_orders_enabled', 'no' ); + + do_action( 'wc_pre_orders_pre_orders_disabled_for_product', $product->get_id() ); + } + } + + /** + * Checks to see if order is zero cost. + * + * @version 1.5.1 + * @since 1.5.1 + * @param object $order + * @return bool + */ + public static function is_zero_cost_order( $order = null ) { + if ( is_a( $order, 'WC_Order' ) ) { + return 0 >= $order->get_total(); + } + + return false; + } + + /** + * Checks to see if order is initially using pay later gateway. + * This is needed because payment method will change later in the process + * when customer goes to pay when product is available. But we need to + * preserve what the order was originally used so we can know if we need + * to reduce stock or not based on that. + * + * @version 1.5.1 + * @since 1.5.1 + * @param object $order + * @return bool + */ + public static function is_order_pay_later( $order_id = null ) { + if ( ! $order_id ) { + return false; + } + + $order = wc_get_order( $order_id ); + return is_object( $order ) && 'yes' === $order->get_meta( '_wc_pre_orders_is_pay_later', true ); + } + + /** + * Completes the pre-order by updating the pre-order status to 'completed' and following this process for handling payment : + * + * - for a pre-order charged upon release AND containing a payment token, an action is fired for the supported gateway + * to hook into an charge the total payment amount. Note that the supported gateway will then call WC_Order::payment_complete() + * upon successful charge + * + * - for a pre-order charged upon release with no payment token, the order status is changed to 'pending' and an email + * is sent containing a link for the customer to come back to and pay for their order + * + * - for a pre-order charged upfront, the order status is changed to 'completed' or 'processing' based on the same rules + * from WC_Order::payment_complete() -- this is because payment_complete() has already occurred for these order + * + * @since 1.0 + * @param int|WC_Order $order post IDs or order object to complete the pre-order for + * @param string $message optional message to include in 'pre-order completed' email to customer + */ + public static function complete_pre_order( $order, $message = '' ) { + + if ( ! is_object( $order ) ) { + $order = new WC_Order( $order ); + } + + if ( ! self::can_pre_order_be_changed_to( 'completed', $order ) ) { + return; + } + + // Save custom customer message in transient to be used in customer email. + // This is needed for orders which get completed directly and updates pre-order status to completed (without message) before we update it in this function (eg: Virtual/downloadable product orders). + // See https://github.com/woocommerce/woocommerce-pre-orders/issues/345 + $transient_key = 'wc_pre_orders_pre_order_completed_message_' . $order->get_id(); + if ( ! empty( $message ) ) { + set_transient( $transient_key, $message, 60 ); + } + + // complete pre-order charged upon release. + if ( WC_Pre_Orders_Order::order_will_be_charged_upon_release( $order ) ) { + $zero_cost_order = self::is_zero_cost_order( $order ); + + $order_id = $order->get_id(); + + if ( ! $zero_cost_order ) { + // Suppress stock increase when status update to pending + remove_action( 'woocommerce_order_status_pending', 'wc_maybe_increase_stock_levels' ); + // update order status to pending so it can be paid by automatic payment, + // or on pay page by customer if 'pay later' gateway was used. + $order->update_status( 'pending' ); + add_action( 'woocommerce_order_status_pending', 'wc_maybe_increase_stock_levels' ); + + if ( WC_Pre_Orders_Order::order_has_payment_token( $order ) || self::is_order_pay_later( $order_id ) ) { + // load payment gateways. + WC()->payment_gateways(); + + // fire action for payment gateway to charge pre-order. + do_action( 'wc_pre_orders_process_pre_order_completion_payment_' . $order->get_payment_method(), $order ); + } + } else { + $product = WC_Pre_Orders_Order::get_pre_order_product( $order ); + + // update order status to completed or processing - based on same process from WC_Order::payment_complete() + if ( ( $product->is_downloadable() && $product->is_virtual() ) || ! apply_filters( 'woocommerce_order_item_needs_processing', true, $product, $order_id ) ) { + $order->update_status( 'completed' ); + } else { + $order->update_status( 'processing' ); + } + + self::reduce_stock_level( $order ); + + } + } else { // Complete pre-order charged upfront. + + $product = WC_Pre_Orders_Order::get_pre_order_product( $order ); + + // update order status to completed or processing - based on same process from WC_Order::payment_complete() + if ( ( $product->is_downloadable() && $product->is_virtual() ) || ! apply_filters( 'woocommerce_order_item_needs_processing', true, $product, $order->get_id() ) ) { + $order->update_status( 'completed' ); + } else { + $order->update_status( 'processing' ); + } + } + + if ( ! empty( $message ) ) { + delete_transient( $transient_key ); + } + // update pre-order status to completed + WC_Pre_Orders_Order::update_pre_order_status( $order, 'completed', $message ); + + do_action( 'wc_pre_orders_pre_order_completed', $order, $message ); + } + + /** + * Completes the provided pre-orders + * + * @since 1.0 + * @param array $orders an array of orders containing a pre-order to complete + * @param string $message optional message to include in 'pre-order completed' email to customer + */ + public static function complete_pre_orders( $orders, $message = '' ) { + foreach ( $orders as $order ) { + self::complete_pre_order( $order, $message ); + } + } + + /** + * Helper function to complete all the pre-orders for a given product + * + * @since 1.0 + * @param object|int $product the product to complete all pre-orders for + * @param string $message an optional message to include in communications to the customer + */ + public static function complete_all_pre_orders( $product, $message ) { + + $orders = self::get_all_pre_orders_by_product( $product ); + + if ( empty( $orders ) ) { + return; + } + + self::complete_pre_orders( $orders, $message ); + } + + /** + * Cancel a pre-orders by changing its order status / pre-order status to 'cancelled' + * + * @since 1.0 + * @param int|WC_Order $order post IDs or order object to cancel the pre-order for + * @param string $message an optional message to include in communications to the customer + */ + public static function cancel_pre_order( $order, $message = '' ) { + + if ( ! is_object( $order ) ) { + $order = new WC_Order( $order ); + } + + if ( ! WC_Pre_Orders_Order::order_contains_pre_order( $order ) ) { + return; + } + + if ( ! self::can_pre_order_be_changed_to( 'cancelled', $order ) ) { + return; + } + + // update the pre-order status + WC_Pre_Orders_Order::update_pre_order_status( $order, 'cancelled', $message ); + + // add 'cancelled' order note for admins + $order->add_order_note( __( 'Pre-order cancelled', 'woocommerce-pre-orders' ) ); + + // update the order status + $order->update_status( 'cancelled' ); + + do_action( 'wc_pre_orders_pre_order_cancelled', $order, $message ); + } + + /** + * Cancels pre-orders by changing their order status / pre-order status to 'cancelled' + * + * @since 1.0 + * @param array $orders array of post IDs or order objects to cancel pre-orders for + * @param string $message an optional message to include in communications to the customer + */ + public static function cancel_pre_orders( $orders, $message = '' ) { + foreach ( $orders as $order ) { + self::cancel_pre_order( $order, $message ); + } + } + + /** + * Helper function to cancel all pre-orders for a given product + * + * @see WC_Pre_Orders_Manager::cancel_pre_orders() + * + * @since 1.0 + * @param object|int $product the product to complete all pre-orders for + * @param string $message an optional message to include in communications to the customer + */ + public static function cancel_all_pre_orders( $product, $message ) { + + $orders = self::get_all_pre_orders_by_product( $product ); + + if ( empty( $orders ) ) { + return; + } + + self::cancel_pre_orders( $orders, $message ); + } + + /** + * Helper function to return a formatted pre-order order total, e.g. '$99 charged on Dec 1, 2014' + * + * @since 1.0 + * @param string $total formatted order total to modify + * @param object|int $product the product that the pre-order contains + * @return string the new formatted order total + */ + public static function get_formatted_pre_order_total( $total, $product ) { + + if ( ! is_object( $product ) ) { + $product = wc_get_product( $product ); + + if ( ! is_object( $product ) ) { + return $total; + } + } + + // get order total format + if ( WC_Pre_Orders_Product::product_is_charged_upon_release( $product ) ) { + $formatted_total = get_option( 'wc_pre_orders_upon_release_order_total_format' ); + } else { + $formatted_total = get_option( 'wc_pre_orders_upfront_order_total_format' ); + } + + // bail if no format is set + if ( ! $formatted_total ) { + return $total; + } + + // add localized availability date if needed + $formatted_total = str_replace( '{availability_date}', WC_Pre_Orders_Product::get_localized_availability_date( $product ), $formatted_total ); + + // add order total + $formatted_total = str_replace( '{order_total}', $total, $formatted_total ); + + return apply_filters( 'wc_pre_orders_pre_order_order_total', $formatted_total, $product ); + } + + /** + * Helper method to disable pre-orders for a product, called after the availability date for a pre-order has been reached + * and pre-orders are completed + * + * @since 1.0 + * @param array|int $product_ids product IDs to disable pre-orders for + */ + private function disable_pre_orders_for_products( $product_ids ) { + + if ( ! is_array( $product_ids ) ) { + $product_ids = array( $product_ids ); + } + + foreach ( $product_ids as $product_id ) { + update_post_meta( $product_id, '_wc_pre_orders_enabled', 'no' ); + + do_action( 'wc_pre_orders_pre_orders_disabled_for_product', $product_id ); + } + } + + /** + * Checks for action to cancel an existing pre order and if needed it gets executed + * + * @since 1.0.3 + */ + public function check_cancel_pre_order() { + global $woocommerce; + + if ( ! isset( $_GET['order_id'] ) || ! isset( $_GET['status'] ) ) { + return; + } + + if ( ! isset( $_GET['_wpnonce'] ) || ! wp_verify_nonce( sanitize_key( $_GET['_wpnonce'] ), absint( $_GET['order_id'] ) ) ) { + return; + } + + self::cancel_pre_order( absint( $_GET['order_id'] ) ); + + $string = __( 'Your pre-order has been cancelled.', 'woocommerce-pre-orders' ); + + // Backwards compatible (pre 2.1) for outputting notice + if ( function_exists( 'wc_add_notice' ) ) { + wc_add_notice( $string ); + } else { + $woocommerce->add_message( $string ); + } + } + + /** + * Generates a unique key used to recognise if an order has been handled by a batch processeor. + * + * @param int[] $product_ids An array of product IDs to generate the hash for. + * @return string A hash of the product IDs. + */ + private static function generate_batch_id( $product_ids ) { + // Sort the product IDs so a consistant hash is generated. + sort( $product_ids ); + return md5( implode( array_values( $product_ids ) ) ); + } + + /** + * When the setting is enabled makes out of stock products into pre-order products + * + * @param int $product_id + * @param string $stock_status + * @param WC_Product $product + */ + public function maybe_activate_preorder( $product_id, $stock_status, $product ) { + global $typenow; + + if ( 'product' !== $typenow && 'outofstock' === $stock_status && 'yes' === get_option( 'wc_pre_orders_auto_pre_order_out_of_stock' ) ) { + + foreach ( $product->get_children() as $child ) { + $child = wc_get_product( $child ); + $child->set_stock_status( 'instock' ); + $child->set_manage_stock( false ); + $child->save(); + } + + update_post_meta( $product->get_id(), '_wc_pre_orders_enabled', 'yes' ); + $product->set_manage_stock( false ); + $product->set_stock_status( 'instock' ); + $product->save(); + } + } + +} // end \WC_Pre_Orders_Manager class diff --git a/includes/class-wc-pre-orders-my-pre-orders.php b/includes/class-wc-pre-orders-my-pre-orders.php new file mode 100644 index 0000000..9690a04 --- /dev/null +++ b/includes/class-wc-pre-orders-my-pre-orders.php @@ -0,0 +1,189 @@ +is_pre_orders_endpoint() ) { + $title = __( 'Pre-orders', 'woocommerce-pre-orders' ); + remove_filter( 'the_title', array( $this, 'endpoint_title' ) ); + } + + return $title; + } + + /** + * Checks if current page is pre-orders endpoint. + * + * @since 1.4.7 + * + * @return bool Returns true if current page is pre-orders endpoint + */ + public function is_pre_orders_endpoint() { + global $wp_query; + + return ( isset( $wp_query->query_vars['pre-orders'] ) + && ! is_admin() + && is_main_query() + && in_the_loop() + && is_account_page() + ); + } + + /** + * Insert Pre-Ordres menu into My Account menus. + * + * @since 1.4.7 + * + * @param array $items Menu items + * + * @return array Menu items + */ + public function menu_items( $items ) { + // Insert Pre-Orders menu. + $new_items = array(); + $new_items['pre-orders'] = __( 'Pre-orders', 'woocommerce-pre-orders' ); + + return $this->_insert_new_items_after( $items, $new_items, 'dashboard' ); + } + + /** + * Helper to add new items into an array after a selected item. + * + * @since 1.4.7 + * + * @param array $items Menu items + * @param array $new_items New menu items + * @param string $after Key in items + * + * @return array Menu items + */ + protected function _insert_new_items_after( $items, $new_items, $after ) { + // Search for the item position and +1 since is after the selected item key. + $position = array_search( $after, array_keys( $items ) ) + 1; + + // Insert the new item. + $array = array_slice( $items, 0, $position, true ); + $array += $new_items; + $array += array_slice( $items, $position, count( $items ) - $position, true ); + + return $array; + } + + /** + * Output "My Pre-Orders" table in the user's My Account page + */ + public function my_pre_orders() { + global $wc_pre_orders; + + $pre_orders = WC_Pre_Orders_Manager::get_users_pre_orders(); + $items = array(); + $actions = array(); + + foreach ( $pre_orders as $order ) { + $_actions = array(); + $order_item = WC_Pre_Orders_Order::get_pre_order_item( $order ); + + // Stop if the pre-order is complete + if ( is_null( $order_item ) ) { + continue; + } + + // Set the items for the table + $items[] = array( + 'order' => $order, + 'data' => $order_item, + ); + + // Determine the available actions (Cancel) + if ( WC_Pre_Orders_Manager::can_pre_order_be_changed_to( 'cancelled', $order ) ) { + $_actions['cancel'] = array( + 'url' => WC_Pre_Orders_Manager::get_users_change_status_link( 'cancelled', $order ), + 'name' => __( 'Cancel', 'woocommerce-pre-orders' ), + ); + } + + $actions[ $order->get_id() ] = $_actions; + } + + // Load the template + wc_get_template( + 'myaccount/my-pre-orders.php', + array( + 'pre_orders' => $pre_orders, + 'items' => $items, + 'actions' => $actions, + ), + '', + $wc_pre_orders->get_plugin_path() . '/templates/' + ); + } +} + +new WC_Pre_Orders_My_Pre_Orders(); diff --git a/includes/class-wc-pre-orders-order.php b/includes/class-wc-pre-orders-order.php new file mode 100644 index 0000000..671aa1c --- /dev/null +++ b/includes/class-wc-pre-orders-order.php @@ -0,0 +1,474 @@ + _x( 'Pre-ordered', 'Order status', 'woocommerce-pre-orders' ), + 'public' => true, + 'exclude_from_search' => false, + 'show_in_admin_all_list' => true, + 'show_in_admin_status_list' => true, + /* translators: %s: number of pre-orders */ + 'label_count' => _n_noop( 'Pre-ordered (%s)', 'Pre-ordered (%s)', 'woocommerce-pre-orders' ), + ) + ); + } + + /** + * Set wc-pre-ordered in WooCommerce order statuses. + * + * @param array $order_statuses + * @return array + */ + public function order_statuses( $order_statuses ) { + $order_statuses['wc-pre-ordered'] = _x( 'Pre-ordered', 'Order status', 'woocommerce-pre-orders' ); + + return $order_statuses; + } + + /** + * Add "pre-ordered" order status to WC Reports for tracking order revenue. + * + * @param array|bool $order_statuses + * @return array + */ + public function add_pre_orders_to_report_statuses( $order_statuses ) { + return is_array( $order_statuses ) ? array_merge( $order_statuses, array( 'pre-ordered' ) ) : $order_statuses; + } + + /** + * Get the order total formatted to show when the order will be (or was) charged + * + * @since 1.0 + * @param string $formatted_total price string ( note: this is already formatted by woocommerce_price() ) + * @param object $order the WC_Order object + * @return string the formatted order total price string + */ + public function get_formatted_order_total( $formatted_total, $order ) { + $product = self::get_pre_order_product( $order ); + + if ( ! empty( $product ) ) { + // only modify the order total on the frontend when the order contains an active pre-order + if ( ! is_admin() && 'active' !== $this->get_pre_order_status( $order ) ) { + $formatted_total = WC_Pre_Orders_Manager::get_formatted_pre_order_total( $formatted_total, $product ); + } + } + + return $formatted_total; + } + + /** + * Checks if an order contains a pre-order + * + * @since 1.0 + * @param object|int $order Preferably the order object, or order ID if + * object is inconvenient to provide. + * @return bool true if the order contains a pre-order, false otherwise + */ + public static function order_contains_pre_order( $order ) { + $order = wc_get_order( $order ); + if ( ! is_object( $order ) ) { + return false; + } + + return (bool) $order->get_meta( '_wc_pre_orders_is_pre_order', true ); + } + + /** + * Checks if an order will be charged upon release + * + * @since 1.0 + * @param object|int $order preferably the order object, or order ID if object is inconvenient to provide + * @return bool true if the order will be charged upon , false otherwise + */ + public static function order_will_be_charged_upon_release( $order ) { + + if ( ! is_object( $order ) ) { + $order = new WC_Order( $order ); + } + + $orders_when_charged = $order->get_meta( '_wc_pre_orders_when_charged', true ); + + if ( ! empty( $orders_when_charged ) ) { + return 'upon_release' === $orders_when_charged; + } + + return WC_Pre_Orders_Product::product_is_charged_upon_release( self::get_pre_order_product( $order ) ); + } + + /** + * Checks if an order requires payment tokenization. For a pre-order charged upon release, a customer has the option + * to use the 'pay later' gateway, and then return and pay for the pre-order with a supported gateway. Because the + * pre-order is still marked as being charged upon release, this helps the supported gateway know how to process the + * payment. + * + * @since 1.0 + * @param object|int $order preferably the order object, or order ID if object is inconvenient to provide + * @return bool true if the order requires payment tokenization , false otherwise + */ + public static function order_requires_payment_tokenization( $order ) { + + if ( ! is_object( $order ) ) { + $order = new WC_Order( $order ); + } + + // if order already has a payment token, tokenization is not required + if ( self::order_has_payment_token( $order ) ) { + return false; + } + + $order_id = $order->get_id(); + + // if the order is charged upon release and no payment token exists then it requires payment tokenization + return ( self::order_will_be_charged_upon_release( $order ) && ! WC_Pre_Orders_Manager::is_order_pay_later( $order_id ) ); + } + + /** + * Checks if an order has an existing payment token that can be used by the original gateway to charge the pre-order + * upon release + * + * @since 1.0 + * @param object|int $order preferably the order object, or order ID if object is inconvenient to provide + * @return bool true if the order contains a payment token , false otherwise + */ + public static function order_has_payment_token( $order ) { + + if ( ! is_object( $order ) ) { + $order = new WC_Order( $order ); + } + + return (bool) $order->get_meta( '_wc_pre_orders_has_payment_token', true ); + } + + /** + * Changes the status for an unpaid, but payment-tokenized order to pre-ordered and adds meta to indicate the order + * has a payment token. Should be used by supported gateways when processing a pre-order charged upon release, instead of calling + * $order->payment_complete(), this will be used. Note that if the order used pay later, this does not apply. + * + * @since 1.0 + * @param object|int $order preferably the order object, or order ID if object is inconvenient to provide + */ + public static function mark_order_as_pre_ordered( $order ) { + + if ( ! is_object( $order ) ) { + $order = new WC_Order( $order ); + } + + $order_id = $order->get_id(); + + if ( WC_Pre_Orders_Manager::is_order_pay_later( $order_id ) ) { + return; + } + + // mark as having a payment token, which will be used upon release to charge pre-order total amount + $order->update_meta_data( '_wc_pre_orders_has_payment_token', 1 ); + + // update status + $order->update_status( 'pre-ordered' ); + + // Save order. + $order->save(); + + // reduce order stock + WC_Pre_Orders_Manager::reduce_stock_level( $order ); + } + + /** + * Since an order may only contain a single pre-ordered item, this returns + * the pre-ordered item array. This method assumes that $order is a pre-order + * + * @since 1.0 + * @version 1.5.3 + * @param object|int $order the order object or order ID + * @return object|bool the pre-ordered order item array + */ + public static function get_pre_order_item( $order ) { + + if ( ! is_object( $order ) ) { + $order = new WC_Order( $order ); + } + + foreach ( $order->get_items( 'line_item' ) as $order_item ) { + + if ( ! empty( $order_item['product_id'] ) ) { + // Avoid running heavy queries via WC_Pre_Orders_Product::product_has_active_pre_orders() that check if any order exists with an active pre-order status to this product if we know the order provided is active. + if ( 'active' === self::get_pre_order_status( $order ) ) { + return $order_item; + } elseif ( WC_Pre_Orders_Product::product_can_be_pre_ordered( $order_item['product_id'] ) || WC_Pre_Orders_Product::product_has_active_pre_orders( $order_item['product_id'] ) ) { + return $order_item; + } + } + } + + return null; + } + + /** + * Since an order may only contain a single pre-ordered product, this returns the pre-ordered product object + * + * @since 1.0 + * @version 1.5.3 + * @param object|int $order preferably the order object, or order ID if object is inconvenient to provide + * @return object|bool the pre-ordered product object, or false if the cart does not contain a pre-order + */ + public static function get_pre_order_product( $order ) { + + if ( ! is_object( $order ) ) { + $order = new WC_Order( $order ); + } + + if ( ! self::order_contains_pre_order( $order ) ) { + return null; + } + + foreach ( $order->get_items( 'line_item' ) as $order_item ) { + if ( ! empty( $order_item['product_id'] ) ) { + // Avoid running heavy queries via WC_Pre_Orders_Product::product_has_active_pre_orders() that check if any order exists with an active pre-order status to this product if we know the order provided is active. + if ( 'active' === self::get_pre_order_status( $order ) ) { + return $order_item->get_product(); + } elseif ( WC_Pre_Orders_Product::product_can_be_pre_ordered( $order_item['product_id'] ) || WC_Pre_Orders_Product::product_has_active_pre_orders( $order_item['product_id'] ) ) { + // return the product object + return $order_item->get_product(); + } + } + } + + return null; + } + + /** + * Get the pre-order status for an order + * - Active = awaiting release + * - Completed = availability date was reached or admin manually completed + * - Cancelled = order and/or pre-order was cancelled + * + * @since 1.0 + * @param object|int $order Preferably the order object, or order ID if + * object is inconvenient to provide. + * @return bool|string The pre-order status or false if order is not valid. + */ + public static function get_pre_order_status( $order ) { + $order = wc_get_order( $order ); + return is_object( $order ) ? $order->get_meta( '_wc_pre_orders_status', true ) : false; + } + + /** + * Returns a pre-order status to display + * + * @since 1.0 + * @param object|int $order preferably the order object, or order ID if object is inconvenient to provide + * @return string the pre-order status for display + */ + public static function get_pre_order_status_to_display( $order ) { + + $status = self::get_pre_order_status( $order ); + + switch ( $status ) { + case 'active': + $status_string = __( 'Active', 'woocommerce-pre-orders' ); + break; + case 'completed': + $status_string = __( 'Completed', 'woocommerce-pre-orders' ); + break; + case 'cancelled': + $status_string = __( 'Cancelled', 'woocommerce-pre-orders' ); + break; + default: + $status_string = apply_filters( 'wc_pre_orders_custom_status_string', ucfirst( $status ), $order ); + break; + } + + return apply_filters( 'wc_pre_orders_status_string', $status_string, $status, $order ); + } + + /** + * Automatically change the pre-order status when the order status changes. + * + * @since 1.0 + * @param int $order_id post ID of the order + * @param string $old_order_status the prior order status + * @param string $new_order_status the new order status + */ + public function auto_update_pre_order_status( $order_id, $old_order_status, $new_order_status ) { + + $order = wc_get_order( $order_id ); + + if ( 'pre-ordered' === $new_order_status && $this->order_contains_pre_order( $order_id ) ) { + $this->update_pre_order_status( $order_id, 'active' ); + } + + if ( 'on-hold' === $new_order_status && $this->order_contains_pre_order( $order_id ) ) { + $this->update_pre_order_status( $order_id, 'active' ); + } + + if ( 'completed' === $new_order_status && $this->order_contains_pre_order( $order_id ) ) { + // Get message to send it to customer email. + $transient_key = 'wc_pre_orders_pre_order_completed_message_' . $order_id; + $message = get_transient( $transient_key ); + if ( ! empty( $message ) ) { + delete_transient( $transient_key ); + } else { + $message = ''; + } + $this->update_pre_order_status( $order_id, 'completed', $message ); + } + + // change to 'cancelled' when changing order status to 'cancelled', except when the pre-order status is already cancelled. this prevents sending double emails when bulk-cancelling pre-orders + if ( 'cancelled' === $new_order_status && self::order_contains_pre_order( $order_id ) && 'cancelled' !== $order->get_meta( '_wc_pre_orders_status', true ) ) { + $this->update_pre_order_status( $order_id, 'cancelled' ); + } + } + + /** + * Update the pre-order status for an order + * + * @since 1.0 + * @param object|int $order preferably the order object, or order ID if object is inconvenient to provide + * @param string $new_status the new pre-order status + * @param string $message an optional message to include in the email to customer + */ + public static function update_pre_order_status( $order, $new_status, $message = '' ) { + if ( ! $new_status ) { + return; + } + + if ( ! is_object( $order ) ) { + $order = new WC_Order( $order ); + } + + $order_id = $order->get_id(); + + $old_status = $order->get_meta( '_wc_pre_orders_status', true ); + + if ( $old_status === $new_status ) { + return; + } + + if ( ! $old_status ) { + $old_status = 'new'; + } + + $order->update_meta_data( '_wc_pre_orders_status', $new_status ); + + // actions for status changes + do_action( 'wc_pre_order_status_' . $new_status, $order_id, $message ); + do_action( 'wc_pre_order_status_' . $old_status . '_to_' . $new_status, $order_id, $message ); + do_action( 'wc_pre_order_status_changed', $order_id, $old_status, $new_status, $message ); + + // Make sure message ends with punctuation and concatenates with status + // transition string. + $message = rtrim( $message ); + if ( ! empty( $message ) ) { + $message .= ! in_array( substr( $message, -1 ), array( '!', '?', '.', ';', ':' ) ) ? '.' : ''; + $message .= ' '; + } + + // add order note + /* translators: %1$s: old pre-order status %2$s: new pre-order status */ + $order->add_order_note( $message . sprintf( __( 'Pre-order status changed from %1$s to %2$s.', 'woocommerce-pre-orders' ), $old_status, $new_status ) ); + + // Save order data + $order->save(); + } + + /** + * Automatically cancel a pre-order if it's parent order is moved to the trash. Note that un-trashing the order does + * not change the pre-order back to it's original status + * + * @since 1.0 + * @param int $order_id the order post ID. + */ + public function maybe_cancel_trashed_pre_order( $order_id ) { + $order = wc_get_order( $order_id ); + if ( ! is_object( $order ) ) { + return; + } + + if ( $this->order_contains_pre_order( $order ) && WC_Pre_Orders_Manager::can_pre_order_be_changed_to( 'cancelled', $order ) ) { + $this->update_pre_order_status( $order, 'cancelled' ); + } + } + + /** + * Product is in stock for orders which are pre-orders (stock reduced during pre-order) + * + * @param bool $in_stock + * @param WC_Product $product + * @param WC_Order $order + * + * @return bool + */ + public static function product_in_stock( $in_stock, $product, $order ) { + if ( self::order_contains_pre_order( $order ) ) { + $in_stock = true; + } + + return $in_stock; + } + +} // end \WC_Pre_Orders_Order class diff --git a/includes/class-wc-pre-orders-privacy.php b/includes/class-wc-pre-orders-privacy.php new file mode 100644 index 0000000..823e66c --- /dev/null +++ b/includes/class-wc-pre-orders-privacy.php @@ -0,0 +1,24 @@ +Learn more about how this works, including what you may want to include in your privacy policy.', 'woocommerce-pre-orders' ), 'https://docs.woocommerce.com/document/marketplace-privacy/#woocommerce-pre-orders' ) ); + } +} + +new WC_Pre_Orders_Privacy(); diff --git a/includes/class-wc-pre-orders-product.php b/includes/class-wc-pre-orders-product.php new file mode 100644 index 0000000..43a03e7 --- /dev/null +++ b/includes/class-wc-pre-orders-product.php @@ -0,0 +1,589 @@ +get_pre_order_product_message( $product, true ); + + return "%s
.', 'woocommerce-pre-orders' ), esc_attr( get_option( 'admin_email' ) ) ),
+ 'placeholder' => '',
+ 'default' => '',
+ ),
+ 'subject' => array(
+ 'title' => __( 'Subject', 'woocommerce-pre-orders' ),
+ 'type' => 'text',
+ /* translators: %s: email subject */
+ 'description' => sprintf( __( 'This controls the email subject line. Leave blank to use the default subject: %s
.', 'woocommerce-pre-orders' ), $this->subject ),
+ 'placeholder' => '',
+ 'default' => '',
+ ),
+ 'heading' => array(
+ 'title' => __( 'Email heading', 'woocommerce-pre-orders' ),
+ 'type' => 'text',
+ /* translators: %s: email heading */
+ 'description' => sprintf( __( 'This controls the main heading contained within the email notification. Leave blank to use the default heading: %s
.', 'woocommerce-pre-orders' ), $this->heading ),
+ 'placeholder' => '',
+ 'default' => '',
+ ),
+ 'email_type' => array(
+ 'title' => __( 'Email type', 'woocommerce-pre-orders' ),
+ 'type' => 'select',
+ 'description' => __( 'Choose which format of email to send.', 'woocommerce-pre-orders' ),
+ 'default' => 'html',
+ 'class' => 'email_type wc-enhanced-select',
+ 'options' => array(
+ 'plain' => __( 'Plain text', 'woocommerce-pre-orders' ),
+ 'html' => __( 'HTML', 'woocommerce-pre-orders' ),
+ 'multipart' => __( 'Multipart', 'woocommerce-pre-orders' ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/includes/emails/class-wc-pre-orders-email-pre-order-available.php b/includes/emails/class-wc-pre-orders-email-pre-order-available.php
new file mode 100644
index 0000000..890943a
--- /dev/null
+++ b/includes/emails/class-wc-pre-orders-email-pre-order-available.php
@@ -0,0 +1,140 @@
+id = 'wc_pre_orders_pre_order_available';
+ $this->title = __( 'Pre-order available', 'woocommerce-pre-orders' );
+ $this->description = __( 'This is an order notification sent to the customer once a pre-order is complete.', 'woocommerce-pre-orders' );
+
+ $this->heading = __( 'Pre-order available', 'woocommerce-pre-orders' );
+ $this->subject = __( 'Your {site_title} pre-order from {order_date} is now available', 'woocommerce-pre-orders' );
+
+ $this->template_base = $wc_pre_orders->get_plugin_path() . '/templates/';
+ $this->template_html = 'emails/customer-pre-order-available.php';
+ $this->template_plain = 'emails/plain/customer-pre-order-available.php';
+
+ // Triggers for this email
+ add_action( 'wc_pre_order_status_completed_notification', array( $this, 'trigger' ), 10, 2 );
+
+ // Call parent constructor
+ parent::__construct();
+ }
+
+
+ /**
+ * Dispatch the email
+ *
+ * @since 1.0
+ */
+ public function trigger( $order_id, $message = '' ) {
+ if ( $order_id ) {
+ $this->object = new WC_Order( $order_id );
+ $this->recipient = $this->object->get_billing_email();
+ $this->message = $message;
+
+ $this->placeholders = array_merge(
+ array(
+ '{order_date}' => date_i18n(
+ wc_date_format(),
+ strtotime( (
+ $this->object->get_date_created() ?
+ gmdate( 'Y-m-d H:i:s', $this->object->get_date_created()->getOffsetTimestamp() )
+ : ''
+ ) )
+ ),
+ '{release_date}' => WC_Pre_Orders_Product::get_localized_availability_date( WC_Pre_Orders_Order::get_pre_order_product( $this->object ) ),
+ '{order_number}' => $this->object->get_order_number()
+ ),
+ $this->placeholders
+ );
+ }
+
+ if ( ! $this->is_enabled() || ! $this->get_recipient() ) {
+ return;
+ }
+
+ $this->send( $this->get_recipient(), $this->get_subject(), $this->get_content(), $this->get_headers(), $this->get_attachments() );
+ }
+
+
+ /**
+ * Gets the email HTML content
+ *
+ * @since 1.0
+ * @return string the email HTML content
+ */
+ public function get_content_html() {
+ ob_start();
+ wc_get_template(
+ $this->template_html,
+ array(
+ 'order' => $this->object,
+ 'email_heading' => $this->get_heading(),
+ 'additional_content' => $this->get_additional_content(),
+ 'message' => $this->message,
+ 'plain_text' => false,
+ 'email' => $this,
+ 'sent_to_admin' => false,
+ ),
+ '',
+ $this->template_base
+ );
+ return ob_get_clean();
+ }
+
+
+ /**
+ * Gets the email plain content
+ *
+ * @since 1.0
+ * @return string the email plain content
+ */
+ public function get_content_plain() {
+ ob_start();
+ wc_get_template(
+ $this->template_plain,
+ array(
+ 'order' => $this->object,
+ 'email_heading' => $this->get_heading(),
+ 'additional_content' => $this->get_additional_content(),
+ 'message' => $this->message,
+ 'plain_text' => true,
+ 'email' => $this,
+ 'sent_to_admin' => false,
+ ),
+ '',
+ $this->template_base
+ );
+ return ob_get_clean();
+ }
+}
diff --git a/includes/emails/class-wc-pre-orders-email-pre-order-cancelled.php b/includes/emails/class-wc-pre-orders-email-pre-order-cancelled.php
new file mode 100644
index 0000000..e30437a
--- /dev/null
+++ b/includes/emails/class-wc-pre-orders-email-pre-order-cancelled.php
@@ -0,0 +1,138 @@
+id = 'wc_pre_orders_pre_order_cancelled';
+ $this->title = __( 'Pre-order cancelled', 'woocommerce-pre-orders' );
+ $this->description = __( 'This is an order notification sent to the customer after a pre-order is cancelled.', 'woocommerce-pre-orders' );
+
+ $this->heading = __( 'Pre-order cancelled', 'woocommerce-pre-orders' );
+ $this->subject = __( 'Your {site_title} pre-order from {order_date} has been cancelled', 'woocommerce-pre-orders' );
+
+ $this->template_base = $wc_pre_orders->get_plugin_path() . '/templates/';
+ $this->template_html = 'emails/customer-pre-order-cancelled.php';
+ $this->template_plain = 'emails/plain/customer-pre-order-cancelled.php';
+
+ // Triggers for this email
+ add_action( 'wc_pre_order_status_active_to_cancelled_notification', array( $this, 'trigger' ), 10, 2 );
+
+ // Call parent constructor
+ parent::__construct();
+ }
+
+
+ /**
+ * Dispatch the email
+ *
+ * @since 1.0
+ */
+ public function trigger( $order_id, $message ) {
+ if ( $order_id ) {
+ $this->object = new WC_Order( $order_id );
+ $this->recipient = $this->object->get_billing_email();
+ $this->message = $message;
+
+ $this->placeholders = array_merge(
+ array(
+ '{order_date}' => date_i18n(
+ wc_date_format(),
+ strtotime( (
+ $this->object->get_date_created() ?
+ gmdate( 'Y-m-d H:i:s', $this->object->get_date_created()->getOffsetTimestamp() )
+ : ''
+ ) )
+ ),
+ '{release_date}' => WC_Pre_Orders_Product::get_localized_availability_date( WC_Pre_Orders_Order::get_pre_order_product( $this->object ) ),
+ '{order_number}' => $this->object->get_order_number()
+ ),
+ $this->placeholders
+ );
+ }
+
+ if ( ! $this->is_enabled() || ! $this->get_recipient() ) {
+ return;
+ }
+
+ $this->send( $this->get_recipient(), $this->get_subject(), $this->get_content(), $this->get_headers(), $this->get_attachments() );
+ }
+
+
+ /**
+ * Gets the email HTML content
+ *
+ * @since 1.0
+ * @return string the email HTML content
+ */
+ public function get_content_html() {
+ ob_start();
+ wc_get_template(
+ $this->template_html,
+ array(
+ 'order' => $this->object,
+ 'email_heading' => $this->get_heading(),
+ 'additional_content' => $this->get_additional_content(),
+ 'message' => $this->message,
+ 'plain_text' => false,
+ 'email' => $this,
+ ),
+ '',
+ $this->template_base
+ );
+ return ob_get_clean();
+ }
+
+
+ /**
+ * Gets the email plain content
+ *
+ * @since 1.0
+ * @return string the email plain content
+ */
+ public function get_content_plain() {
+ ob_start();
+ wc_get_template(
+ $this->template_plain,
+ array(
+ 'order' => $this->object,
+ 'email_heading' => $this->get_heading(),
+ 'additional_content' => $this->get_additional_content(),
+ 'message' => $this->message,
+ 'plain_text' => true,
+ 'email' => $this,
+ 'sent_to_admin' => false,
+ ),
+ '',
+ $this->template_base
+ );
+ return ob_get_clean();
+ }
+}
diff --git a/includes/emails/class-wc-pre-orders-email-pre-order-date-changed.php b/includes/emails/class-wc-pre-orders-email-pre-order-date-changed.php
new file mode 100644
index 0000000..23373b6
--- /dev/null
+++ b/includes/emails/class-wc-pre-orders-email-pre-order-date-changed.php
@@ -0,0 +1,157 @@
+id = 'wc_pre_orders_pre_order_date_changed';
+ $this->title = __( 'Pre-order release date changed', 'woocommerce-pre-orders' );
+ $this->description = __( 'This is an order notification sent to the customer after a pre-order release date is changed.', 'woocommerce-pre-orders' );
+
+ $this->heading = __( 'Pre-order release date changed', 'woocommerce-pre-orders' );
+ $this->subject = __( 'The release date for your {site_title} pre-order from {order_date} has been changed', 'woocommerce-pre-orders' );
+
+ $this->template_base = $wc_pre_orders->get_plugin_path() . '/templates/';
+ $this->template_html = 'emails/customer-pre-order-date-changed.php';
+ $this->template_plain = 'emails/plain/customer-pre-order-date-changed.php';
+
+ // Triggers for this email
+ add_action( 'wc_pre_orders_pre_order_date_changed_notification', array( $this, 'trigger' ), 10 );
+
+ // Call parent constructor
+ parent::__construct();
+ }
+
+
+ /**
+ * Dispatch the email
+ *
+ * @since 2.0.7 Update logic to handle function first argument as array.
+ * First argument is same as for wc_pre_orders_pre_order_date_changed action hook.
+ * @since 1.0
+ */
+ public function trigger( array $args ) {
+ if (
+ ! array_key_exists( 'order', $args )
+ || ! array_key_exists( 'message', $args )
+ || ! $this->is_enabled()
+ ) {
+ return;
+ }
+
+ $this->object = $args['order'];
+ $this->recipient = $this->object->get_billing_email();
+ $this->message = $args['message'];
+ $this->availability_date = WC_Pre_Orders_Product::get_localized_availability_date( WC_Pre_Orders_Order::get_pre_order_product( $this->object ) );
+
+ $this->placeholders = array_merge(
+ array(
+ '{order_date}' => date_i18n(
+ wc_date_format(),
+ strtotime(
+ $this->object->get_date_created()
+ ? gmdate( 'Y-m-d H:i:s', $this->object->get_date_created()->getOffsetTimestamp() )
+ : ''
+ )
+ ),
+ '{release_date}' => WC_Pre_Orders_Product::get_localized_availability_date( WC_Pre_Orders_Order::get_pre_order_product( $this->object ) ),
+ '{order_number}' => $this->object->get_order_number(),
+ ),
+ $this->placeholders
+ );
+
+ if ( $this->get_recipient() ) {
+ $this->send(
+ $this->get_recipient(),
+ $this->get_subject(),
+ $this->get_content(),
+ $this->get_headers(),
+ $this->get_attachments()
+ );
+ }
+ }
+
+
+ /**
+ * Gets the email HTML content
+ *
+ * @since 1.0
+ * @return string the email HTML content
+ */
+ public function get_content_html() {
+ ob_start();
+ wc_get_template(
+ $this->template_html,
+ array(
+ 'order' => $this->object,
+ 'email_heading' => $this->get_heading(),
+ 'additional_content' => $this->get_additional_content(),
+ 'message' => $this->message,
+ 'availability_date' => $this->availability_date,
+ 'plain_text' => false,
+ 'email' => $this,
+ ),
+ '',
+ $this->template_base
+ );
+ return ob_get_clean();
+ }
+
+
+ /**
+ * Gets the email plain content
+ *
+ * @since 1.0
+ * @return string the email plain content
+ */
+ public function get_content_plain() {
+ ob_start();
+ wc_get_template(
+ $this->template_plain,
+ array(
+ 'order' => $this->object,
+ 'email_heading' => $this->get_heading(),
+ 'additional_content' => $this->get_additional_content(),
+ 'message' => $this->message,
+ 'availability_date' => $this->availability_date,
+ 'plain_text' => true,
+ 'email' => $this,
+ 'sent_to_admin' => false,
+ ),
+ '',
+ $this->template_base
+ );
+ return ob_get_clean();
+ }
+}
diff --git a/includes/emails/class-wc-pre-orders-email-pre-ordered.php b/includes/emails/class-wc-pre-orders-email-pre-ordered.php
new file mode 100644
index 0000000..c610df6
--- /dev/null
+++ b/includes/emails/class-wc-pre-orders-email-pre-ordered.php
@@ -0,0 +1,141 @@
+id = 'wc_pre_orders_pre_ordered';
+ $this->title = __( 'Pre-ordered', 'woocommerce-pre-orders' );
+ $this->description = __( 'This is an order notification sent to the customer after placing a pre-order and containing order details.', 'woocommerce-pre-orders' );
+
+ $this->heading = __( 'Thank you for your pre-order', 'woocommerce-pre-orders' );
+ $this->subject = __( 'Your {site_title} pre-order confirmation from {order_date}', 'woocommerce-pre-orders' );
+
+ $this->template_base = $wc_pre_orders->get_plugin_path() . '/templates/';
+ $this->template_html = 'emails/customer-pre-ordered.php';
+ $this->template_plain = 'emails/plain/customer-pre-ordered.php';
+
+ // Triggers for this email
+ add_action( 'wc_pre_order_status_new_to_active_notification', array( $this, 'trigger' ) );
+
+ // Call parent constructor
+ parent::__construct();
+ }
+
+
+ /**
+ * Dispatch the email
+ *
+ * @since 1.0
+ */
+ public function trigger( $order_id ) {
+ if ( $order_id ) {
+ $this->object = new WC_Order( $order_id );
+ $this->recipient = $this->object->get_billing_email();
+ $this->availability_date = WC_Pre_Orders_Product::get_localized_availability_date( WC_Pre_Orders_Order::get_pre_order_product( $this->object ),
+ __( 'a future date', 'woocommerce-pre-orders' ) );
+
+ $this->placeholders = array_merge(
+ array(
+ '{order_date}' => date_i18n(
+ wc_date_format(),
+ strtotime( (
+ $this->object->get_date_created() ?
+ gmdate( 'Y-m-d H:i:s', $this->object->get_date_created()->getOffsetTimestamp() )
+ : ''
+ ) )
+ ),
+ '{release_date}' => WC_Pre_Orders_Product::get_localized_availability_date( WC_Pre_Orders_Order::get_pre_order_product( $this->object ) ),
+ '{order_number}' => $this->object->get_order_number()
+ ),
+ $this->placeholders
+ );
+ }
+
+ if ( ! $this->is_enabled() || ! $this->get_recipient() ) {
+ return;
+ }
+
+ $this->send( $this->get_recipient(), $this->get_subject(), $this->get_content(), $this->get_headers(), $this->get_attachments() );
+ }
+
+
+ /**
+ * Gets the email HTML content
+ *
+ * @since 1.0
+ * @return string the email HTML content
+ */
+ public function get_content_html() {
+ ob_start();
+ wc_get_template(
+ $this->template_html,
+ array(
+ 'order' => $this->object,
+ 'email_heading' => $this->get_heading(),
+ 'additional_content' => $this->get_additional_content(),
+ 'availability_date' => $this->availability_date,
+ 'plain_text' => false,
+ 'email' => $this,
+ ),
+ '',
+ $this->template_base
+ );
+ return ob_get_clean();
+ }
+
+
+ /**
+ * Gets the email plain content
+ *
+ * @since 1.0
+ * @return string the email plain content
+ */
+ public function get_content_plain() {
+ ob_start();
+ wc_get_template(
+ $this->template_plain,
+ array(
+ 'order' => $this->object,
+ 'email_heading' => $this->get_heading(),
+ 'additional_content' => $this->get_additional_content(),
+ 'availability_date' => $this->availability_date,
+ 'plain_text' => true,
+ 'email' => $this,
+ 'sent_to_admin' => false,
+ ),
+ '',
+ $this->template_base
+ );
+ return ob_get_clean();
+ }
+}
diff --git a/includes/gateways/class-wc-pre-orders-gateway-pay-later.php b/includes/gateways/class-wc-pre-orders-gateway-pay-later.php
new file mode 100644
index 0000000..ed57d06
--- /dev/null
+++ b/includes/gateways/class-wc-pre-orders-gateway-pay-later.php
@@ -0,0 +1,175 @@
+id = 'pre_orders_pay_later';
+ $this->method_title = __( 'pay later', 'woocommerce-pre-orders' );
+ $this->method_description = __( 'This payment method replaces all other methods that do not support pre-orders when the pre-order is charged upon release.', 'woocommerce-pre-orders' );
+ $this->icon = apply_filters( 'wc_pre_orders_pay_later_icon', '' );
+ $this->has_fields = false;
+
+ // Load the settings
+ $this->init_form_fields();
+ $this->init_settings();
+
+ $this->enabled = $this->get_option( 'enabled', 'yes' );
+ $this->title = $this->get_option( 'title' );
+ $this->description = $this->get_option( 'description' );
+
+ // Support pre-orders
+ $this->supports = array( 'products', 'pre-orders' );
+
+ // Save settings
+ if ( is_admin() ) {
+ add_action( 'woocommerce_update_options_payment_gateways_' . $this->id, array( $this, 'process_admin_options' ) );
+ }
+
+ // pay page fallback
+ add_action( 'woocommerce_receipt_' . $this->id, array( $this, 'receipt_page' ) );
+ }
+
+
+ /**
+ * Disables the gateway under any of these conditions:
+ * 1) If the cart does not contain a pre-order
+ * 2) If the pre-order amount is charged upfront
+ * 3) On the pay page
+ *
+ * @since 1.0
+ * @return bool
+ */
+ public function is_available() {
+
+ $is_available = true;
+
+ // Backwards compatibility checking for payment page
+ if ( function_exists( 'is_checkout_pay_page' ) ) {
+ $pay_page = is_checkout_pay_page();
+ } else {
+ $pay_page = is_page( wc_get_page_id( 'pay' ) );
+ }
+
+ // On checkout page
+ if ( ! $pay_page || ( defined( 'WOOCOMMERCE_CHECKOUT' ) && WOOCOMMERCE_CHECKOUT ) ) {
+
+ // Not available if the cart does not contain a pre-order
+ if ( WC_Pre_Orders_Cart::cart_contains_pre_order() ) {
+
+ // Not available when the pre-order amount is charged upfront
+ if ( WC_Pre_Orders_Product::product_is_charged_upfront( WC_Pre_Orders_Cart::get_pre_order_product() ) ) {
+ $is_available = false;
+ }
+ } else {
+
+ $is_available = false;
+ }
+ } else {
+
+ // Not available on the pay page (for now)
+ $is_available = false;
+ }
+
+ return $is_available;
+ }
+
+
+ /**
+ * Setup gateway form fields
+ *
+ * @since 1.0
+ */
+ public function init_form_fields() {
+ $this->form_fields = array(
+ 'enabled' => array(
+ 'title' => __( 'Enable/Disable', 'woocommerce-pre-orders' ),
+ 'label' => __( 'Enable pay later', 'woocommerce-pre-orders' ),
+ 'type' => 'checkbox',
+ 'description' => '',
+ 'default' => 'yes',
+ ),
+ 'title' => array(
+ 'title' => __( 'Title', 'woocommerce-pre-orders' ),
+ 'type' => 'text',
+ 'description' => __( 'This controls the title which the user sees during checkout.', 'woocommerce-pre-orders' ),
+ 'default' => __( 'Pay later', 'woocommerce-pre-orders' ),
+ 'desc_tip' => true,
+ ),
+ 'description' => array(
+ 'title' => __( 'Customer message', 'woocommerce-pre-orders' ),
+ 'type' => 'textarea',
+ 'description' => __( 'Let the customer know how they will be able to pay for their pre-order.', 'woocommerce-pre-orders' ),
+ 'default' => __( 'You will receive an email when the pre-order is available along with instructions on how to complete your order.', 'woocommerce-pre-orders' ),
+ ),
+ );
+ }
+
+
+ /**
+ * Process the payment and return the result
+ *
+ * @since 1.0
+ *
+ * @param int $order_id
+ *
+ * @return array
+ */
+ public function process_payment( $order_id ) {
+ $order = new WC_Order( $order_id );
+
+ // Remove cart
+ WC()->cart->empty_cart();
+
+ // Update status
+ $order->update_status( 'pre-ordered' );
+
+ // Add a flag the order used pay later.
+ $order->update_meta_data( '_wc_pre_orders_is_pay_later', 'yes' );
+ $order->save();
+
+ WC_Pre_Orders_Manager::reduce_stock_level( $order );
+
+ // Redirect to thank you page
+ return array(
+ 'result' => 'success',
+ 'redirect' => $this->get_return_url( $order ),
+ );
+ }
+
+ /**
+ * Receipt page.
+ *
+ * @param WC_Order $order
+ *
+ * @return string
+ */
+ public function receipt_page( $order ) {
+ echo '' . esc_html__( 'Thank you for your order.', 'woocommerce-pre-orders' ) . '
'; + } + +} // end \WC_Pre_Orders_Gateway_Pay_Later class diff --git a/includes/shortcodes/class-wc-pre-orders-shortcode-countdown.php b/includes/shortcodes/class-wc-pre-orders-shortcode-countdown.php new file mode 100644 index 0000000..16e2b2f --- /dev/null +++ b/includes/shortcodes/class-wc-pre-orders-shortcode-countdown.php @@ -0,0 +1,229 @@ +shortcode_wrapper( array( __CLASS__, 'output' ), $atts, array( 'class' => 'woocommerce-pre-orders' ) ); + } + + /** + * Sanitize the layout content. + * + * @param string $content Layout content + * @return string + */ + private static function sanitize_layout( $content ) { + $content = wp_kses_no_null( $content, array( 'slash_zero' => 'keep' ) ); + $content = wp_kses_normalize_entities( $content ); + $content = preg_replace_callback( '%(|$))|(<(?!})[^>]*(>|$))%', array( __CLASS__, 'sanitize_layout_callback' ), $content ); + + // This sanitization comes from the `esc_js` function in WordPress. + // The same sanitization is used except for `_wp_specialchars` which removes characters needed for HTML. + // https://core.trac.wordpress.org/browser/tags/6.2/src/wp-includes/formatting.php#L4548 + $content = wp_check_invalid_utf8( $content ); + $content = preg_replace( '/(x)?0*(?(1)27|39);?/i', "'", stripslashes( $content ) ); + $content = str_replace( "\r", '', $content ); + return str_replace( "\n", '\\n', addslashes( $content ) ); + } + + /** + * Callback for `sanitize_layout()`. + * + * @param array $matches preg_replace regexp matches + * @return string + */ + public static function sanitize_layout_callback( $matches ) { + $allowed_html = wp_kses_allowed_html( 'post' ); + $allowed_protocols = wp_allowed_protocols(); + + return wp_kses_split2( $matches[0], $allowed_html, $allowed_protocols ); + } + + /** + * Output the countdown timer. This defaults to the following format, where + * elments in [ ] are not shown if zero: + * + * [y Years] [o Months] [d Days] h Hours m Minutes s Seconds + * + * The following shortcode arguments are optional: + * + * * product_id/product_sku - id or sku of pre-order product to countdown to. + * Defaults to current product, if any + * * until - date/time to count down to, overrides product release date + * if set. Example values: "15 March 2015", "+1 month". + * More examples: http://php.net/manual/en/function.strtotime.php + * * before - text to show before the countdown. Only available if 'layout' is not '' + * * after - text to show after the countdown. Only available if 'layout' is not '' + * * layout - The countdown layout, defaults to y Years o Months d Days h Hours m Minutes s Seconds + * See http://keith-wood.name/countdownRef.html#layout for all possible options + * * format - The format for the countdown display. Example: 'yodhms' + * to display the year, month, day and time. See http://keith-wood.name/countdownRef.html#format for all options + * * compact - If 'true' displays the date/time labels in compact form, ie + * 'd' rather than 'days'. Defaults to 'false' + * + * When the countdown date/time is reached the page will refresh. + * + * To test different time periods you can create shortcodes like the following samples: + * + * [woocommerce_pre_order_countdown until="+10 year"] + * [woocommerce_pre_order_countdown until="+10 month"] + * [woocommerce_pre_order_countdown until="+10 day"] + * [woocommerce_pre_order_countdown until="+10 second"] + * + * @param array $atts associative array of shortcode parameters + */ + public static function get_pre_order_countdown_shortcode_content( $atts ) { + global $woocommerce, $product, $wpdb; + + $shortcode_atts = shortcode_atts( + array( + 'product_id' => '', + 'product_sku' => '', + 'until' => '', + 'before' => '', + 'after' => '', + 'layout' => '{y<}{yn} {yl}{y>} {o<}{on} {ol}{o>} {d<}{dn} {dl}{d>} {h<}{hn} {hl}{h>} {m<}{mn} {ml}{m>} {s<}{sn} {sl}{s>}', + 'format' => 'yodHMS', + 'compact' => 'false', + ), + $atts + ); + + $product_id = $shortcode_atts['product_id']; + + // product by sku? + if ( $shortcode_atts['product_sku'] ) { + $product_id = wc_get_product_id_by_sku( $shortcode_atts['product_sku'] ); + } + + // product by id? + if ( $product_id ) { + $product = wc_get_product( $product_id ); + } + + // no product, or product is in the trash? Bail. + if ( ! $product instanceof WC_Product || 'trash' === $product->get_status() ) { + return; + } + + // date override (convert from string unless someone was savvy enough to provide a timestamp) + $until = $shortcode_atts['until']; + if ( $until && ! is_numeric( $until ) ) { + $until = strtotime( $until ); + } + + // no date override, get the datetime from the product. + if ( ! $until ) { + $until = get_post_meta( $product->get_id(), '_wc_pre_orders_availability_datetime', true ); + } + + // can't do anything without an 'until' date + if ( ! $until ) { + return; + } + + // if a layout is being used, prepend/append the before/after text + $layout = $shortcode_atts['layout']; + if ( $layout ) { + $layout = esc_js( $shortcode_atts['before'] ); + $layout .= self::sanitize_layout( $shortcode_atts['layout'] ); + $layout .= esc_js( $shortcode_atts['after'] ); + } + + // enqueue the required javascripts + self::enqueue_scripts(); + + // countdown javascript + ob_start(); + ?> + $('#woocommerce-pre-orders-countdown-').countdown({ + until: new Date(), + layout: '', + format: '', + compact: , + expiryUrl: location.href, + }); + add_inline_js( $javascript ); + } + + ob_start(); + ?> +%s
."
+msgstr ""
+
+#: includes/emails/class-wc-pre-orders-email-new-pre-order.php:159
+msgid "Subject"
+msgstr ""
+
+#: includes/emails/class-wc-pre-orders-email-new-pre-order.php:162
+#. translators: %s: email subject
+msgid ""
+"This controls the email subject line. Leave blank to use the default "
+"subject: %s
."
+msgstr ""
+
+#: includes/emails/class-wc-pre-orders-email-new-pre-order.php:167
+msgid "Email heading"
+msgstr ""
+
+#: includes/emails/class-wc-pre-orders-email-new-pre-order.php:170
+#. translators: %s: email heading
+msgid ""
+"This controls the main heading contained within the email notification. "
+"Leave blank to use the default heading: %s
."
+msgstr ""
+
+#: includes/emails/class-wc-pre-orders-email-new-pre-order.php:175
+msgid "Email type"
+msgstr ""
+
+#: includes/emails/class-wc-pre-orders-email-new-pre-order.php:177
+msgid "Choose which format of email to send."
+msgstr ""
+
+#: includes/emails/class-wc-pre-orders-email-new-pre-order.php:181
+msgid "Plain text"
+msgstr ""
+
+#: includes/emails/class-wc-pre-orders-email-new-pre-order.php:182
+msgid "HTML"
+msgstr ""
+
+#: includes/emails/class-wc-pre-orders-email-new-pre-order.php:183
+msgid "Multipart"
+msgstr ""
+
+#: includes/emails/class-wc-pre-orders-email-pre-order-available.php:36
+#: includes/emails/class-wc-pre-orders-email-pre-order-available.php:39
+msgid "Pre-order available"
+msgstr ""
+
+#: includes/emails/class-wc-pre-orders-email-pre-order-available.php:37
+msgid ""
+"This is an order notification sent to the customer once a pre-order is "
+"complete."
+msgstr ""
+
+#: includes/emails/class-wc-pre-orders-email-pre-order-available.php:40
+msgid "Your {site_title} pre-order from {order_date} is now available"
+msgstr ""
+
+#: includes/emails/class-wc-pre-orders-email-pre-order-cancelled.php:36
+msgid ""
+"This is an order notification sent to the customer after a pre-order is "
+"cancelled."
+msgstr ""
+
+#: includes/emails/class-wc-pre-orders-email-pre-order-cancelled.php:39
+msgid "Your {site_title} pre-order from {order_date} has been cancelled"
+msgstr ""
+
+#: includes/emails/class-wc-pre-orders-email-pre-order-date-changed.php:39
+#: includes/emails/class-wc-pre-orders-email-pre-order-date-changed.php:42
+msgid "Pre-order release date changed"
+msgstr ""
+
+#: includes/emails/class-wc-pre-orders-email-pre-order-date-changed.php:40
+msgid ""
+"This is an order notification sent to the customer after a pre-order "
+"release date is changed."
+msgstr ""
+
+#: includes/emails/class-wc-pre-orders-email-pre-order-date-changed.php:43
+msgid ""
+"The release date for your {site_title} pre-order from {order_date} has been "
+"changed"
+msgstr ""
+
+#: includes/emails/class-wc-pre-orders-email-pre-ordered.php:37
+msgid "Pre-ordered"
+msgstr ""
+
+#: includes/emails/class-wc-pre-orders-email-pre-ordered.php:38
+msgid ""
+"This is an order notification sent to the customer after placing a "
+"pre-order and containing order details."
+msgstr ""
+
+#: includes/emails/class-wc-pre-orders-email-pre-ordered.php:40
+msgid "Thank you for your pre-order"
+msgstr ""
+
+#: includes/emails/class-wc-pre-orders-email-pre-ordered.php:41
+msgid "Your {site_title} pre-order confirmation from {order_date}"
+msgstr ""
+
+#: includes/emails/class-wc-pre-orders-email-pre-ordered.php:65
+msgid "a future date"
+msgstr ""
+
+#: includes/gateways/class-wc-pre-orders-gateway-pay-later.php:32
+msgid "pay later"
+msgstr ""
+
+#: includes/gateways/class-wc-pre-orders-gateway-pay-later.php:33
+msgid ""
+"This payment method replaces all other methods that do not support "
+"pre-orders when the pre-order is charged upon release."
+msgstr ""
+
+#: includes/gateways/class-wc-pre-orders-gateway-pay-later.php:111
+msgid "Enable pay later"
+msgstr ""
+
+#: includes/gateways/class-wc-pre-orders-gateway-pay-later.php:117
+msgid "Title"
+msgstr ""
+
+#: includes/gateways/class-wc-pre-orders-gateway-pay-later.php:119
+msgid "This controls the title which the user sees during checkout."
+msgstr ""
+
+#: includes/gateways/class-wc-pre-orders-gateway-pay-later.php:126
+msgid "Let the customer know how they will be able to pay for their pre-order."
+msgstr ""
+
+#: includes/gateways/class-wc-pre-orders-gateway-pay-later.php:172
+msgid "Thank you for your order."
+msgstr ""
+
+#: templates/emails/admin-new-pre-order.php:25
+#: templates/emails/plain/admin-new-pre-order.php:24
+#. translators: %s: billing first name and last name
+#. translators: %s: first name and last name
+msgid "You have received a pre-order from %s. Their pre-order is as follows:"
+msgstr ""
+
+#: templates/emails/admin-pre-order-cancelled.php:31
+#: templates/emails/plain/admin-pre-order-cancelled.php:21
+#. Translators: %s Full name of the customer
+msgid ""
+"A pre-order from %s has been cancelled. The order details are shown below "
+"for your reference."
+msgstr ""
+
+#: templates/emails/customer-pre-order-available.php:29
+#. translators: %1$s: href link for checkout payment url %2$s: closing href
+#. link
+msgid ""
+"Your pre-order is now available, but requires payment. %1$sPlease pay for "
+"your pre-order now.%2$s"
+msgstr ""
+
+#: templates/emails/customer-pre-order-available.php:36
+#: templates/emails/plain/customer-pre-order-available.php:29
+msgid ""
+"Your pre-order is now available, but is waiting for the payment to be "
+"confirmed. Please wait until it's confirmed. Optionally, make sure the "
+"related payment has been sent to avoid delays on your order."
+msgstr ""
+
+#: templates/emails/customer-pre-order-available.php:44
+#. translators: %1$s: href link for checkout payment url %2$s: closing href
+#. link
+msgid ""
+"Your pre-order is now available, but automatic payment failed. %1$sPlease "
+"update your payment information now.%2$s"
+msgstr ""
+
+#: templates/emails/customer-pre-order-available.php:50
+#: templates/emails/plain/customer-pre-order-available.php:37
+msgid ""
+"Your pre-order is now available. Your order details are shown below for "
+"your reference."
+msgstr ""
+
+#: templates/emails/customer-pre-order-available.php:91
+#: templates/emails/plain/customer-pre-order-available.php:78
+msgid "Thanks for shopping with us."
+msgstr ""
+
+#: templates/emails/customer-pre-order-cancelled.php:24
+#: templates/emails/plain/customer-pre-order-cancelled.php:22
+msgid ""
+"Your pre-order has been cancelled. Your order details are shown below for "
+"your reference."
+msgstr ""
+
+#: templates/emails/customer-pre-order-date-changed.php:27
+#: templates/emails/plain/customer-pre-order-date-changed.php:24
+#. translators: %s: availability date
+msgid ""
+"Your pre-order release date has been changed. The new release date is %s. "
+"Your order details are shown below for your reference."
+msgstr ""
+
+#: templates/emails/customer-pre-order-date-changed.php:32
+#: templates/emails/plain/customer-pre-order-date-changed.php:26
+msgid ""
+"Your pre-order release date has been changed. Your order details are shown "
+"below for your reference."
+msgstr ""
+
+#: templates/emails/customer-pre-ordered.php:42
+#: templates/emails/plain/customer-pre-ordered.php:36
+#. translators: %s: availability date
+msgid ""
+"Your pre-order has been received. You will be notified when your pre-order "
+"is released%s Your order details are shown below for your reference."
+msgstr ""
+
+#: templates/emails/plain/customer-pre-order-available.php:25
+msgid ""
+"Your pre-order is now available, but requires payment. Please pay for your "
+"pre-order now: "
+msgstr ""
+
+#: templates/emails/plain/customer-pre-order-available.php:33
+msgid ""
+"Your pre-order is now available, but automatic payment failed. Please "
+"update your payment information now : "
+msgstr ""
+
+#: templates/myaccount/my-pre-orders.php:20
+#: templates/myaccount/my-pre-orders.php:56
+msgid "Release date"
+msgstr ""
+
+#: templates/myaccount/my-pre-orders.php:76
+msgid "You have no pre-orders."
+msgstr ""
+
+#: woocommerce-pre-orders.php:39
+#. translators: %s WC download URL link.
+msgid ""
+"Pre-orders require WooCommerce to be installed and active. You can download "
+"%s here."
+msgstr ""
+
+#. Plugin Name of the plugin/theme
+msgid "WooCommerce Pre-Orders"
+msgstr ""
+
+#. Plugin URI of the plugin/theme
+msgid "https://woocommerce.com/products/woocommerce-pre-orders/"
+msgstr ""
+
+#. Description of the plugin/theme
+msgid "Sell pre-orders for products in your WooCommerce store."
+msgstr ""
+
+#. Author of the plugin/theme
+msgid "WooCommerce"
+msgstr ""
+
+#. Author URI of the plugin/theme
+msgid "https://woocommerce.com"
+msgstr ""
+
+#: includes/admin/class-wc-pre-orders-admin-orders.php:206
+#: includes/admin/class-wc-pre-orders-admin-orders.php:231
+msgctxt "An order type"
+msgid "Non Pre-Orders"
+msgstr ""
+
+#: includes/admin/class-wc-pre-orders-admin-orders.php:207
+#: includes/admin/class-wc-pre-orders-admin-orders.php:232
+msgctxt "An order type"
+msgid "Pre-Orders Only"
+msgstr ""
+
+#: includes/class-wc-pre-orders-order.php:71
+#: includes/class-wc-pre-orders-order.php:89
+msgctxt "Order status"
+msgid "Pre-ordered"
+msgstr ""
\ No newline at end of file
diff --git a/src/admin/blocks/date-time-picker/block.json b/src/admin/blocks/date-time-picker/block.json
new file mode 100644
index 0000000..537c0e0
--- /dev/null
+++ b/src/admin/blocks/date-time-picker/block.json
@@ -0,0 +1,34 @@
+{
+ "$schema": "https://schemas.wp.org/trunk/block.json",
+ "apiVersion": 3,
+ "name": "woocommerce-pre-orders/date-time-picker",
+ "version": "0.1.0",
+ "title": "Date-time picker",
+ "category": "widgets",
+ "icon": "flag",
+ "description": "A block to select the date-time",
+ "attributes": {
+ "property": {
+ "type": "string",
+ "default": ""
+ },
+ "title": {
+ "type": "string",
+ "default": ""
+ },
+ "help": {
+ "type": "string",
+ "default": ""
+ },
+ "date": {
+ "type": "object",
+ "default": null
+ }
+ },
+ "supports": {
+ "html": false,
+ "inserter": false
+ },
+ "textdomain": "woocommerce-pre-orders",
+ "editorScript": "file:./index.js"
+}
diff --git a/src/admin/blocks/date-time-picker/edit.tsx b/src/admin/blocks/date-time-picker/edit.tsx
new file mode 100644
index 0000000..ae293cb
--- /dev/null
+++ b/src/admin/blocks/date-time-picker/edit.tsx
@@ -0,0 +1,98 @@
+/**
+ * External dependencies
+ */
+import { useWooBlockProps } from '@woocommerce/block-templates';
+import {
+ TextControl,
+ DateTimePicker,
+ Popover,
+ Card,
+ CardBody,
+ Button,
+ Flex,
+ FlexItem,
+ BaseControl,
+} from '@wordpress/components';
+import { useState } from '@wordpress/element';
+import {
+ __experimentalUseProductEntityProp as useProductEntityProp,
+} from '@woocommerce/product-editor';
+import { __ } from '@wordpress/i18n';
+
+export function Edit({ attributes, context: { postType } } ) {
+ const blockProps = useWooBlockProps( attributes );
+ const {
+ title,
+ property,
+ help,
+ date,
+ disabled,
+ } = attributes;
+
+ const [ value, setValue ] = useProductEntityProp+get_billing_first_name() . ' ' . $order->get_billing_last_name(); +/* translators: %s: billing first name and last name */ +printf( esc_html__( 'You have received a pre-order from %s. Their pre-order is as follows:', 'woocommerce-pre-orders' ), $full_name ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped +?> +
+ + + + + + + + diff --git a/templates/emails/admin-pre-order-cancelled.php b/templates/emails/admin-pre-order-cancelled.php new file mode 100644 index 0000000..941529f --- /dev/null +++ b/templates/emails/admin-pre-order-cancelled.php @@ -0,0 +1,76 @@ + + + + ++get_billing_first_name() . ' ' . $order->get_billing_last_name(); + +/* Translators: %s Full name of the customer */ +printf( esc_html__( 'A pre-order from %s has been cancelled. The order details are shown below for your reference.', 'woocommerce-pre-orders' ), esc_html( $full_name ) ); +?> +
+ + ++ ++ + + diff --git a/templates/emails/customer-pre-order-available.php b/templates/emails/customer-pre-order-available.php new file mode 100644 index 0000000..eec8bfb --- /dev/null +++ b/templates/emails/customer-pre-order-available.php @@ -0,0 +1,102 @@ + + + + +get_status() && ! WC_Pre_Orders_Manager::is_zero_cost_order( $order ) ) : + ?> + +
+ get_checkout_payment_url() ) . '">', '' ); + ?> +
+ +get_status() && ! WC_Pre_Orders_Manager::is_zero_cost_order( $order ) ) : ?> + ++ +
+ +get_status() ) : ?> + ++ get_checkout_payment_url() ) . '">', '' ); + ?> +
+ + + + + + + + + + + + ++ +
+ + + + ++ +
+ + + + + + + + + + + + + + diff --git a/templates/emails/customer-pre-order-date-changed.php b/templates/emails/customer-pre-order-date-changed.php new file mode 100644 index 0000000..36d5a0c --- /dev/null +++ b/templates/emails/customer-pre-order-date-changed.php @@ -0,0 +1,55 @@ + + + + + ++ +
+ ++ +
+ + + + + + + + + + + + + + + diff --git a/templates/emails/customer-pre-ordered.php b/templates/emails/customer-pre-ordered.php new file mode 100644 index 0000000..8afb0e4 --- /dev/null +++ b/templates/emails/customer-pre-ordered.php @@ -0,0 +1,63 @@ + + + + + + + ' . sprintf( esc_html__( 'Your pre-order has been received. You will be automatically charged for your order via your selected payment method when your pre-order is released%s Your order details are shown below for your reference.', 'woocommerce-pre-orders' ), $availability_date_text ) . ''; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } else { + /* translators: %s: availability date */ + echo '' . sprintf( esc_html__( 'Your pre-order has been received. You will be prompted for payment for your order when your pre-order is released%s Your order details are shown below for your reference.', 'woocommerce-pre-orders' ), $availability_date_text ) . '
'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } + ?> + + ++ +
+ + + + + + + + + + + + diff --git a/templates/emails/plain/admin-new-pre-order.php b/templates/emails/plain/admin-new-pre-order.php new file mode 100644 index 0000000..fe9360c --- /dev/null +++ b/templates/emails/plain/admin-new-pre-order.php @@ -0,0 +1,57 @@ +get_billing_first_name() . ' ' . $order->get_billing_last_name(); +/* translators: %s: first name and last name */ +echo sprintf( esc_html__( 'You have received a pre-order from %s. Their pre-order is as follows:', 'woocommerce-pre-orders' ), esc_html( $full_name ) ) . "\n\n"; + +/* + * @hooked WC_Emails::order_details() Shows the order details table. + * @hooked WC_Structured_Data::generate_order_data() Generates structured data. + * @hooked WC_Structured_Data::output_structured_data() Outputs structured data. + * @since 2.5.0 + */ +do_action( 'woocommerce_email_order_details', $order, $sent_to_admin, $plain_text, $email ); + +echo "\n----------------------------------------\n\n"; + +/* + * @hooked WC_Emails::order_meta() Shows order meta data. + */ +do_action( 'woocommerce_email_order_meta', $order, $sent_to_admin, $plain_text, $email ); + +/* + * @hooked WC_Emails::customer_details() Shows customer details + * @hooked WC_Emails::email_address() Shows email address + */ +do_action( 'woocommerce_email_customer_details', $order, $sent_to_admin, $plain_text, $email ); + +echo "\n\n----------------------------------------\n\n"; + +/** + * Show user-defined additional content - this is set in each email's settings. + */ +if ( $additional_content ) { + echo esc_html( wp_strip_all_tags( wptexturize( $additional_content ) ) ); + echo "\n\n----------------------------------------\n\n"; +} + +echo wp_kses_post( apply_filters( 'woocommerce_email_footer_text', get_option( 'woocommerce_email_footer_text' ) ) ); diff --git a/templates/emails/plain/admin-pre-order-cancelled.php b/templates/emails/plain/admin-pre-order-cancelled.php new file mode 100644 index 0000000..8c00fd1 --- /dev/null +++ b/templates/emails/plain/admin-pre-order-cancelled.php @@ -0,0 +1,70 @@ +get_billing_first_name() . ' ' . $order->get_billing_last_name(); +/* Translators: %s Full name of the customer */ +printf( esc_html__( 'A pre-order from %s has been cancelled. The order details are shown below for your reference.', 'woocommerce-pre-orders' ), esc_html( $full_name ) ) . "\n\n"; + +if ( $message ) : + + echo "----------\n\n"; + echo esc_html( wp_strip_all_tags( wptexturize( $message ) ) ) . "\n\n"; + echo "----------\n\n"; + +endif; + +/** + * @hooked WC_Emails::order_details() Shows the order details table. + * @hooked WC_Structured_Data::generate_order_data() Generates structured data. + * @hooked WC_Structured_Data::output_structured_data() Outputs structured data. + * @since 2.5.0 + */ +do_action( 'woocommerce_email_order_details', $order, $sent_to_admin, $plain_text, $email ); + +echo "\n----------------------------------------\n\n"; + +/** + * @hooked WC_Emails::order_meta() Shows order meta data. + * + * @since 1.7.3 + */ +do_action( 'woocommerce_email_order_meta', $order, $sent_to_admin, $plain_text, $email ); + +/** + * @hooked WC_Emails::customer_details() Shows customer details + * @hooked WC_Emails::email_address() Shows email address + * @since 1.7.3 + */ +do_action( 'woocommerce_email_customer_details', $order, $sent_to_admin, $plain_text, $email ); + +echo "\n\n----------------------------------------\n\n"; + +/** + * Show user-defined additional content - this is set in each email's settings. + */ +if ( $additional_content ) { + echo esc_html( wp_strip_all_tags( wptexturize( $additional_content ) ) ); + echo "\n\n----------------------------------------\n\n"; +} + +/** + * @hooked woocommerce_email_footer_text modifies the footer text of the email + * + * @since 1.7.3 + */ +echo wp_kses_post( apply_filters( 'woocommerce_email_footer_text', get_option( 'woocommerce_email_footer_text' ) ) ); diff --git a/templates/emails/plain/customer-pre-order-available.php b/templates/emails/plain/customer-pre-order-available.php new file mode 100644 index 0000000..9e8f546 --- /dev/null +++ b/templates/emails/plain/customer-pre-order-available.php @@ -0,0 +1,81 @@ +get_status() && ! WC_Pre_Orders_Manager::is_zero_cost_order( $order ) ) : + + esc_html_e( 'Your pre-order is now available, but requires payment. Please pay for your pre-order now: ', 'woocommerce-pre-orders' ) . esc_url( $order->get_checkout_payment_url() ) . "\n\n"; + +elseif ( 'on-hold' === $order->get_status() && ! WC_Pre_Orders_Manager::is_zero_cost_order( $order ) ) : + + esc_html_e( "Your pre-order is now available, but is waiting for the payment to be confirmed. Please wait until it's confirmed. Optionally, make sure the related payment has been sent to avoid delays on your order.", 'woocommerce-pre-orders' ) . "\n\n"; + +elseif ( 'failed' === $order->get_status() ) : + + esc_html_e( 'Your pre-order is now available, but automatic payment failed. Please update your payment information now : ', 'woocommerce-pre-orders' ) . esc_url( $order->get_checkout_payment_url() ) . "\n\n"; + +else : + + esc_html_e( 'Your pre-order is now available. Your order details are shown below for your reference.', 'woocommerce-pre-orders' ) . "\n\n"; + +endif; + +if ( $message ) : + + echo "----------\n\n"; + echo esc_html( wp_strip_all_tags( wptexturize( $message ) ) ) . "\n\n"; + echo "----------\n\n"; + +endif; + +/* + * @hooked WC_Emails::order_details() Shows the order details table. + * @hooked WC_Structured_Data::generate_order_data() Generates structured data. + * @hooked WC_Structured_Data::output_structured_data() Outputs structured data. + * @since 2.5.0 + */ +do_action( 'woocommerce_email_order_details', $order, $sent_to_admin, $plain_text, $email ); + +echo "\n----------------------------------------\n\n"; + +/* + * @hooked WC_Emails::order_meta() Shows order meta data. + */ +do_action( 'woocommerce_email_order_meta', $order, $sent_to_admin, $plain_text, $email ); + +/* + * @hooked WC_Emails::customer_details() Shows customer details + * @hooked WC_Emails::email_address() Shows email address + */ +do_action( 'woocommerce_email_customer_details', $order, $sent_to_admin, $plain_text, $email ); + +echo "\n\n----------------------------------------\n\n"; + +/** + * Show user-defined additional content - this is set in each email's settings. + */ +if ( $additional_content ) { + echo esc_html( wp_strip_all_tags( wptexturize( $additional_content ) ) ); +} else { + echo esc_html__( 'Thanks for shopping with us.', 'woocommerce-pre-orders' ); +} + +echo wp_kses_post( apply_filters( 'woocommerce_email_footer_text', get_option( 'woocommerce_email_footer_text' ) ) ); diff --git a/templates/emails/plain/customer-pre-order-cancelled.php b/templates/emails/plain/customer-pre-order-cancelled.php new file mode 100644 index 0000000..1d8fd43 --- /dev/null +++ b/templates/emails/plain/customer-pre-order-cancelled.php @@ -0,0 +1,63 @@ + + + ++ | + | + | + | + |
---|---|---|---|---|
+ + + #get_order_number() ); ?> + + + + get_order_number() ); ?> + + + | ++ + + + | ++ + | ++ + | ++ $action ) : // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited ?> + + + | +
' . sprintf( esc_html__( 'Pre-orders require WooCommerce to be installed and active. You can download %s here.', 'woocommerce-pre-orders' ), 'WooCommerce' ) . '
'; +} + +// When plugin is activated. +register_activation_hook( __FILE__, 'woocommerce_pre_orders_activate' ); + +/** + * Actions to perform when plugin is activated. + * + * @since 1.4.7 + */ +function woocommerce_pre_orders_activate() { + add_rewrite_endpoint( 'pre-orders', EP_ROOT | EP_PAGES ); + flush_rewrite_rules(); +} + +if ( ! class_exists( 'WC_Pre_Orders' ) ) : + define( 'WC_PRE_ORDERS_VERSION', '2.1.0' ); // WRCS: DEFINED_VERSION. + define( 'WC_PRE_ORDERS_PLUGIN_PATH', untrailingslashit( plugin_dir_path( __FILE__ ) ) ); + define( 'WC_PRE_ORDERS_PLUGIN_URL', untrailingslashit( plugins_url( basename( plugin_dir_path( __FILE__ ), basename( __FILE__ ) ) ) ) ); + define( 'WC_PRE_ORDERS_GUTENBERG_EXISTS', function_exists( 'register_block_type' ) ? true : false ); + require 'includes/class-wc-pre-orders.php'; +endif; + +add_action( 'plugins_loaded', 'woocommerce_pre_orders_init' ); + +/** + * Initializes the extension. + * + * @return Object Instance of the extension. + * @since 1.5.25 + */ +function woocommerce_pre_orders_init() { + load_plugin_textdomain( 'woocommerce-pre-orders', false, plugin_basename( dirname( __FILE__ ) ) . '/languages' ); + + if ( ! class_exists( 'WooCommerce' ) ) { + add_action( 'admin_notices', 'woocommerce_pre_orders_missing_wc_notice' ); + + return; + } + + $GLOBALS['wc_pre_orders'] = new WC_Pre_Orders( __FILE__ ); +} + +add_action( 'plugins_loaded', 'woocommerce_pre_orders_init' ); + +add_filter( 'woocommerce_translations_updates_for_' . basename( __FILE__, '.php' ), '__return_true' ); + +/** + * Loads the classes for the integration with WooCommerce Blocks. + */ +function woocommerce_pre_orders_load_block_classes() { + if ( class_exists( 'Automattic\WooCommerce\Blocks\Package' ) && version_compare( \Automattic\WooCommerce\Blocks\Package::get_version(), '6.5.0', '>' ) ) { + \WC_Pre_Orders::load_block_classes(); + } +} + +add_action( 'woocommerce_blocks_loaded', 'woocommerce_pre_orders_load_block_classes' );