name = apply_filters( 'charitable_gateway_square_name', __( 'Square', 'charitable' ) );
$this->defaults = array(
'label' => __( 'Square', 'charitable' ),
);
$this->badge = __( 'Recommended', 'charitable' );
$this->recommended = true;
$this->supports = array(
'1.3.0',
'credit-card',
'recurring',
'refunds',
);
$this->connect = ( new Charitable_Square_Connect() )->init();
$this->connection = Charitable_Square_Connection::get( charitable_get_option( 'test_mode' ) ? 'test' : 'live' );
$this->is_connected = $this->is_square_connected( charitable_get_option( 'test_mode' ) ? 'test' : 'live' );
// Add action for handling redirects.
add_action( 'admin_init', array( $this, 'handle_redirect' ) );
// Add action for checking token status.
add_action( 'admin_init', array( $this, 'check_token_status' ), 10 );
}
/**
* Check the locations.
*
* @since 1.8.7
*/
public function check_locations() {
// Only check this if page=charitable-settings&tab=gateways&group=gateways_square_core.
if ( ! isset( $_GET['page'] ) || 'charitable-settings' !== $_GET['page'] || ! isset( $_GET['tab'] ) || 'gateways' !== $_GET['tab'] || ! isset( $_GET['group'] ) || 'gateways_square_core' !== $_GET['group'] ) { //phpcs:ignore
return;
}
// Check if the Square Connect class exists.
if ( ! class_exists( 'Charitable_Square_Connect' ) ) {
return;
}
// Check if the Square API class exists.
$mode = charitable_get_option( 'test_mode' ) ? 'test' : 'live';
$locations = $this->get_location_options( $mode );
}
/**
* Handle redirects for Square gateway settings.
*
* @since 1.8.7
*/
public function handle_redirect() {
if ( isset( $_GET['redirect'] ) && isset( $_GET['page'] ) && 'charitable-settings' === $_GET['page'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
$mode = charitable_get_option( 'test_mode' ) ? 'test' : 'live';
$connect = new Charitable_Square_Connect();
$connect->prepare_locations( $mode );
wp_safe_redirect(
add_query_arg(
array(
'page' => 'charitable-settings',
'tab' => 'gateways',
'group' => 'gateways_square_core',
),
admin_url( 'admin.php' )
)
);
exit;
}
}
/**
* Register the Square payment gateway class.
*
* @since 1.8.7
*
* @param string[] $gateways The list of registered gateways.
* @return string[]
*/
public static function register_gateway( $gateways ) {
// Check if PHP version is compatible with Square SDK.
if ( class_exists( 'Charitable_Square_Compatibility' ) && ! Charitable_Square_Compatibility::is_compatible() ) {
// Don't register Square gateway if PHP version is incompatible.
if ( charitable_is_debug( 'square' ) ) {
// phpcs:disable
error_log( '[Charitable Square Core] Gateway not registered - PHP version incompatible' );
// phpcs:enable
}
return $gateways;
}
// Only register if not already registered (prevents duplicate registration).
if ( ! isset( $gateways['square_core'] ) ) {
$gateways['square_core'] = 'Charitable_Gateway_Square';
if ( charitable_is_debug( 'square' ) ) {
// phpcs:disable
error_log( '[Charitable Square Core] Gateway registered successfully' );
// phpcs:enable
}
}
return $gateways;
}
/**
* Get the API object.
*
* @since 1.8.7
*
* @param boolean|null $test_mode Whether to explicitly get the test or live key.
* @param string|null $live_access_token The live access token.
* @param string|null $test_access_token The test access token.
* @return \Charitable_Square_API
*/
public static function api( $test_mode = null, $live_access_token = null, $test_access_token = null ) {
if ( is_null( $test_mode ) ) {
return null;
}
return new \Charitable_Square_API( $test_mode, $live_access_token, $test_access_token );
}
/**
* This processes a request from the user to manually renew the Square token.
*
* @since 1.8.7
*
* @return void
*/
public function square_renew_token() {
$mode = isset( $_GET['mode'] ) ? sanitize_text_field( wp_unslash( $_GET['mode'] ) ) : 'test'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ( 'sandbox' === $mode ) {
$mode = 'test';
}
if ( empty( $_GET['action'] ) || 'square_renew_token' !== $_GET['action'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
return;
}
// Get current live and/or access tokens.
$live_access_token = charitable_square_get_access_token( 'live' );
$test_access_token = charitable_square_get_access_token( 'test' );
// Get the refresh token.
$refresh_token = $this->get_personal_access_refresh_token( $mode );
if ( empty( $refresh_token ) ) {
wp_die( esc_html__( 'No refresh token found.', 'charitable' ) );
return;
}
// Renew the token using the API.
$response = $this->api( $mode, $live_access_token, $test_access_token )->renew_token( $refresh_token );
if ( false === $response ) {
wp_die( esc_html__( 'Failed to renew token. Please try again.', 'charitable' ) );
return;
}
// Update the stored tokens.
$settings = get_option( 'charitable_settings' );
$settings['gateways_square'][ $mode ]['access_token'] = $response['access_token'];
// Update refresh token if a new one was provided.
if ( isset( $response['refresh_token'] ) ) {
$settings[ $mode ]['refresh_token'] = $response['refresh_token'];
}
update_option( 'charitable_settings', $settings );
// Redirect back to settings page with success message.
wp_safe_redirect( $this->get_settings_page_url() );
exit;
}
/**
* Register gateway settings.
*
* @since 1.8.7
*
* @param array[] $settings Default array of settings for the gateway.
* @return array[]
*/
public function gateway_settings( $settings ) {
if ( $this->connect === null ) {
$this->connect = new Charitable_Square_Connect();
}
$connected_status_css = $this->is_connected ? 'square-connected' : 'square-disconnected';
$settings = array_merge(
$settings,
array(
'square_connection_status' => array(
'type' => 'content',
'title' => __( 'Connection Status', 'charitable' ),
'content' => $this->get_connection_status_content( $this->connection, charitable_get_option( 'test_mode' ) ? 'test' : 'live' ),
'priority' => 10,
'class' => 'square-connection-status square-non-legacy',
),
'square_business_locations' => array(
'type' => 'content',
'title' => __( 'Business Locations', 'charitable' ),
'content' => $this->get_business_locations_content(),
'class' => 'square-business-locations square-non-legacy ' . $connected_status_css,
'priority' => 50,
),
'square_webhooks' => array(
'type' => 'content',
'title' => __( 'Enable Webhooks', 'charitable' ),
'content' => $this->get_webhooks_content(),
'priority' => 60,
'class' => 'square-webhooks-content square-non-legacy',
),
)
);
$settings = apply_filters( 'charitable_square_gateway_settings', $settings );
return $settings;
}
/**
* Show the link to the documentation about the Webhooks ID.
*
* @since 1.8.7
*
* @param string $mode Square mode (e.g. 'live' or 'sandbox').
*
* @return string
*/
private function get_webhooks_id_desc( string $mode ): string {
$modes = array(
'live' => __( 'Live Mode Endpoint Subscription ID', 'charitable' ),
'sandbox' => __( 'Test Mode Endpoint Subscription ID', 'charitable' ),
);
return sprintf(
wp_kses( /* translators: %1$s - Live Mode Endpoint ID or Test Mode Endpoint ID. %2$s - Square Dashboard Webhooks Settings URL. */
__( 'Retrieve your %1$s from your Square webhook settings . Select the endpoint, then click Copy button.', 'charitable' ),
array(
'a' => array(
'href' => array(),
'target' => array(),
'rel' => array(),
),
)
),
$modes[ $mode ],
esc_url( self::SQUARE_APPS_URL )
);
}
/**
* Show the link to the documentation about the Webhook Signature Key.
*
* @since 1.8.7
*
* @param string $mode Square mode (e.g. 'live' or 'sandbox').
*
* @return string
*/
private function get_webhooks_secret_desc( string $mode ): string {
$modes = array(
'live' => __( 'Live Mode Signature Key', 'charitable' ),
'sandbox' => __( 'Test Mode Signature Key', 'charitable' ),
);
return sprintf(
wp_kses( /* translators: %1$s - Live Mode Signing Secret or Test Mode Signing Secret. %2$s - Square Dashboard Webhooks Settings URL. */
__( 'Retrieve your %1$s from your Square webhook settings . Select the endpoint, then click Reveal.', 'charitable' ),
array(
'a' => array(
'href' => array(),
'target' => array(),
'rel' => array(),
),
)
),
$modes[ $mode ],
esc_url( self::SQUARE_APPS_URL )
);
}
/**
* Determine if Square is connected.
*
* @since 1.8.7
*
* @return int
*/
public function maybe_square_connected() {
return $this->check_keys_exist( 'live' ) || $this->check_keys_exist( 'test' );
}
/**
* Determine if Square is connected.
*
* @since 1.8.7
*
* @param string $mode The mode to check for keys. Should be 'test' or 'live'. If it's sandbox, rename it to 'test'.
* @return int
*/
private function is_square_connected( string $mode = 'test' ) {
if ( ! $this->connection ) {
return false;
}
// If the connection is invalid, return false.
if ( ! $this->connection->is_valid() ) {
return false;
}
return true;
}
/**
* Retrieve a Connection Status setting content.
*
* @since 1.8.7
*
* @param Charitable_Square_Connection $connection Connection data.
* @param string $mode Square mode.
*
* @return string
*/
private function get_connection_status_content( $connection = null, $mode = 'test' ): string {
if ( ! $this->is_square_connected() ) {
return $this->get_disconnected_status_content( $mode );
}
$content = $this->get_disabled_status_content( $connection );
if ( ! empty( $content ) ) {
return $content;
}
return $this->get_enabled_status_content( $connection );
}
/**
* Retrieve a Disconnected Status setting content.
*
* @since 1.8.7
*
* @param string $mode Square mode.
*
* @return string
*/
private function get_disconnected_status_content( string $mode ): string {
return $this->get_fees_content() .
$this->get_connect_button( $mode, false ) .
'
' .
sprintf(
wp_kses( /* translators: %s - Charitable.com Square documentation article URL. */
__( 'Securely connect to Square with just a few clicks to begin accepting payments! Learn More ', 'charitable' ),
array(
'a' => array(
'href' => array(),
'target' => array(),
'rel' => array(),
'class' => array(),
),
)
),
esc_url( charitable_utm_link( 'https://wpcharitable.com/docs/how-to-install-and-use-the-square-addon-with-charitable/#connect-square', 'Settings - Payments', 'Square Learn More' ) )
) .
'
';
}
/**
* Retrieve setting content when a connection is disabled.
*
* @since 1.8.7
*
* @param Charitable_Square_Connection $connection Connection data.
*
* @return string
*/
private function get_disabled_status_content( Charitable_Square_Connection $connection ): string {
if ( ! $connection->is_configured() ) {
return $this->get_missing_status_content( $connection->get_mode() );
}
if ( ! $connection->is_valid() ) {
return $this->get_invalid_status_content( $connection->get_mode() );
}
return '';
}
/**
* Retrieve a connection is missing status content.
*
* @since 1.8.7
*
* @param string $mode Square mode.
*
* @return string
*/
private function get_missing_status_content( string $mode ): string {
return $this->get_fees_content() . '' . $this->get_error_icon() . esc_html__( 'Your connection is missing required data. You must reconnect your Square account.', 'charitable' ) . $this->get_disconnect_button( $mode ) . '
';
}
/**
* Retrieve a connection invalid status content.
*
* @since 1.8.7
*
* @param string $mode Square mode.
*
* @return string
*/
private function get_invalid_status_content( string $mode ): string {
return $this->get_fees_content() . '' . $button . '
' : $button;
}
/**
* Generate the Square Connect button.
*
* @since 1.8.7
*
* @param string $mode The api mode (test|live).
*
* @return string
*/
public function get_square_connect_button_html( $mode = 'test' ) {
$html = '';
if ( 'live' === $mode ) {
$html .= '' . wp_kses_post( $help ) . '
'; //phpcs:ignore
return ob_get_clean();
}
/**
* Get the "Connect Webhooks" button.
*
* @since 1.8.7
*
* @return string
*/
private function get_webhooks_connect_button() {
// Assume webhooks are configured manually in Square dashboard.
$is_connected = true;
if ( $is_connected ) {
return sprintf(
'';
if ( charitable_get_option( 'test_mode' ) ) {
$html .= '
' . __( 'Charitable is in test mode and currently using the Sandbox environment.', 'charitable' ) . ' ';
} else {
$html .= '
' . __( 'You have setup your Square account in Sandbox mode. Charitable is in live mode so this is not being used', 'charitable' ) . ' ';
}
$html .= '
' . __( 'The Sandbox environment is where all operations are simulated and no actual charges will occur. Perfect for testing your setup.', 'charitable' ) . '
';
$html .= '
' . __( 'Disconnect from Square Sandbox.', 'charitable' ) . '
';
$html .= '
';
$html .= '
';
$html .= '
';
$html .= '
';
// check if the locations are set.
$square_locations_check = get_option( '_charitable_square_locations_test' );
if ( ! empty( $square_locations_check ) ) {
$html .= '
';
$html .= '
';
}
return $html;
}
/**
* Get the Square access token.
*
* @since 1.8.7
*
* @param string $mode The api mode, test or live.
* @return string
*/
public function get_square_access_token( $mode = 'test' ) {
$access_token = charitable_square_get_refresh_token( $mode );
// Debug log the settings.
if ( charitable_is_debug( 'square' ) ) {
// phpcs:disable
error_log(
sprintf(
'[Charitable Square] Retrieving access token for mode: %s',
$mode
)
);
$square_settings = charitable_get_option( 'gateways_square', array() );
error_log(
sprintf(
'[Charitable Square] Settings: %s',
wp_json_encode( $square_settings )
)
);
// phpcs:enable
}
if ( ! empty( $access_token ) ) {
// Debug log token presence.
if ( charitable_is_debug( 'square' ) ) {
// phpcs:disable
error_log(
sprintf(
'[Charitable Square] Found access token: %s',
substr( $access_token, 0, 10 ) . '...' // Only log first 10 chars for security.
)
);
// phpcs:enable
}
return $access_token;
}
// Debug log missing token.
if ( charitable_is_debug( 'square' ) ) {
// phpcs:disable
error_log(
sprintf(
'[Charitable Square] No access token found for mode: %s',
$mode
)
);
// phpcs:enable
}
return '';
}
/**
* Get the Square refresh token.
*
* @since 1.8.7
*
* @param string $mode The api mode, test or live.
* @return string
*/
public function get_square_refresh_token( $mode = 'test' ) {
return charitable_square_get_refresh_token( $mode );
}
/**
* Get the Square merchant ID.
*
* @since 1.8.7
*
* @param string $mode The api mode, test or live.
* @return string
*/
public function get_square_merchant_id( $mode = 'test' ) {
$square_settings = charitable_get_option( 'gateways_square', array() );
if ( ! empty( $square_settings[ $mode ]['merchant_id'] ) ) {
return $square_settings[ $mode ]['merchant_id'];
}
return '';
}
/**
* Get the URL to connect to Square.
*
* @since 1.8.7
*
* @param string $mode The mode to connect to.
* @param string $redirect_url The URL to redirect to after connecting.
* @return string
*/
public function get_square_connect_url( $mode = '', $redirect_url = '' ) {
if ( ! $mode ) {
$mode = 'test' === charitable_get_option( 'test_mode' ) || 1 === charitable_get_option( 'test_mode' ) ? 'test' : 'live';
}
if ( empty( $redirect_url ) ) {
$redirect_url = add_query_arg(
array(
'tab' => 'gateways',
'page' => 'charitable-settings',
'group' => 'gateways_square_core',
),
admin_url( 'admin.php' )
);
}
// Get the scopes for the needed permissions.
$scopes = Charitable_Square_Connect::get_instance()->get_scopes();
return add_query_arg(
array(
'live_mode' => $mode === 'live' ? 1 : 0,
'state' => str_pad( wp_rand( wp_rand(), PHP_INT_MAX ), 100, wp_rand(), STR_PAD_BOTH ),
'customer_site_url' => rawurlencode( $redirect_url ),
'admin_email' => get_bloginfo( 'admin_email' ),
'permissions' => implode( '+', $scopes ),
),
'https://connect.wpcharitable.com/square-connect/?wpcharitable_gateway_connect_init=square_connect'
);
}
/**
* Get the URL to renew the Square token.
*
* @since 1.8.7
*
* @param string $redirect_url The URL to redirect to after connecting.
* @param boolean $cron_job_request Whether the request is a cron job request.
* @return string
*/
public function get_square_renew_token_url( $redirect_url = '', $cron_job_request = false ) {
if ( empty( $redirect_url ) ) {
$redirect_url = add_query_arg(
array(
'tab' => 'gateways',
'page' => 'charitable-settings',
'group' => 'gateways_square_core',
),
admin_url( 'admin.php' )
);
}
$mode = 'live' === charitable_get_option( 'test_mode' ) ? 'live' : 'test';
return add_query_arg(
array(
'live_mode' => (int) ! charitable_get_option( 'test_mode' ),
'refresh_token' => charitable_square_get_refresh_token( $mode ),
'key' => get_option( 'charitable_square_aes_key_' . $mode, '' ),
'customer_site_url' => urlencode( $redirect_url ), // phpcs:ignore
'admin_email' => get_bloginfo( 'admin_email' ),
'cron_job_request' => $cron_job_request,
),
'https://connect.wpcharitable.com/square-connect/?wpcharitable_gateway_connect_refresh_token=square_connect' // todo: filter market site url.
);
}
/**
* Get the URL to disconnect from Square.
*
* @since 1.8.7
*
* @param string $mode The mode to disconnect from.
* @param string $redirect_url The URL to redirect to after disconnecting.
* @return string
*/
public function get_square_disconnect_url( $mode = 'sandbox', $redirect_url = '' ) {
return add_query_arg(
array(
'wpcharitable-square-disconnect' => true,
'_wpnonce' => wp_create_nonce(
'wpcharitable-square-connect-disconnect'
),
'tab' => 'gateways',
'page' => 'charitable-settings',
'group' => 'gateways_square_core',
'mode' => $mode,
),
admin_url( 'admin.php' )
);
}
/**
* Returns the current gateway's ID.
*
* @return string
* @since 1.8.7
*/
public static function get_gateway_id() {
return self::ID;
}
/**
* Save more complex Square gateway settings.
*
* @since 1.8.7
*
* @param array $values The values to save.
* @param array $new_values The new values.
* @param array $old_values The old values.
* @return array
*/
public function save_settings( $values, $new_values, $old_values ) {
$square_settings_root_old_settings = ! empty( $old_values['gateways_square'] ) ? $old_values['gateways_square'] : array();
$square_settings_root_new_settings = ! empty( $new_values['gateways_square'] ) ? $new_values['gateways_square'] : array();
// merge the new settings with the old settings, with the new settings taking precedence.
$values['gateways_square'] = array_merge( $square_settings_root_old_settings, $square_settings_root_new_settings );
// Handle location ID for test mode.
if ( isset( $_POST['charitable_settings']['gateways_square']['test']['location_id'] ) ) { //phpcs:ignore
// For the location ID we should save the location ID to the option for test mode.
update_option( 'charitable_square_location_id_test', sanitize_text_field( $_POST['charitable_settings']['gateways_square']['test']['location_id'] ) ); //phpcs:ignore
$values['gateways_square']['test']['location_id'] = sanitize_text_field( $_POST['charitable_settings']['gateways_square']['test']['location_id'] ); //phpcs:ignore
}
// Handle location ID for live mode.
if ( isset( $_POST['charitable_settings']['gateways_square']['live']['location_id'] ) ) { //phpcs:ignore
// For the location ID we should save the location ID to the option for live mode.
update_option( 'charitable_square_location_id_live', sanitize_text_field( $_POST['charitable_settings']['gateways_square']['live']['location_id'] ) ); //phpcs:ignore
$values['gateways_square']['live']['location_id'] = sanitize_text_field( $_POST['charitable_settings']['gateways_square']['live']['location_id'] ); //phpcs:ignore
}
// We need to possibly update the sandbox and live webhook ids and secrets.
// If the values exist in $_POST that means the webhook_settings=1 was added to the settings URL.
if ( isset( $_POST['charitable_settings']['gateways_square']['square_webhooks_id_sandbox'] ) ) { //phpcs:ignore
if ( ! empty( $_POST['charitable_settings']['gateways_square']['square_webhooks_id_sandbox'] ) ) { //phpcs:ignore
$values['gateways_square']['test']['webhooks-id'] = sanitize_text_field( $_POST['charitable_settings']['gateways_square']['square_webhooks_id_sandbox'] ); //phpcs:ignore
} else {
$values['gateways_square']['test']['webhooks-id'] = '';
}
}
if ( isset( $_POST['charitable_settings']['gateways_square']['square_webhooks_secret_sandbox'] ) ) { //phpcs:ignore
if ( ! empty( $_POST['charitable_settings']['gateways_square']['square_webhooks_secret_sandbox'] ) ) { //phpcs:ignore
$values['gateways_square']['test']['webhooks-secret'] = sanitize_text_field( $_POST['charitable_settings']['gateways_square']['square_webhooks_secret_sandbox'] ); //phpcs:ignore
} else {
$values['gateways_square']['test']['webhooks-secret'] = '';
}
}
if ( isset( $_POST['charitable_settings']['gateways_square']['square_webhooks_id_live'] ) ) { //phpcs:ignore
if ( ! empty( $_POST['charitable_settings']['gateways_square']['square_webhooks_id_live'] ) ) { //phpcs:ignore
$values['gateways_square']['live']['webhooks-id'] = sanitize_text_field( $_POST['charitable_settings']['gateways_square']['square_webhooks_id_live'] ); //phpcs:ignore
} else {
$values['gateways_square']['live']['webhooks-id'] = '';
}
}
if ( isset( $_POST['charitable_settings']['gateways_square']['square_webhooks_secret_live'] ) ) { //phpcs:ignore
if ( ! empty( $_POST['charitable_settings']['gateways_square']['square_webhooks_secret_live'] ) ) { //phpcs:ignore
$values['gateways_square']['live']['webhooks-secret'] = sanitize_text_field( $_POST['charitable_settings']['gateways_square']['square_webhooks_secret_live'] ); //phpcs:ignore
} else {
$values['gateways_square']['live']['webhooks-secret'] = '';
}
}
// Check if we should clear the mode connection warning.
$this->maybe_clear_mode_connection_warning( $values );
return $values;
}
/**
* Get the personal access token.
*
* @since 1.8.7
*
* @param string $mode The mode to get the personal access token for.
* @return string
*/
public function get_personal_access_token( $mode = 'test' ) {
// Personal access tokens are no longer used - return empty string.
return '';
}
/**
* Get the personal access refresh token.
*
* @since 1.8.7
*
* @param string $mode The mode to get the personal access refresh token for.
* @return string
*/
public function get_personal_access_refresh_token( $mode = 'test' ) {
// Personal access tokens are no longer used - return empty string.
return '';
}
/**
* Get the personal access token refresh date.
*
* @since 1.8.7
*
* @param string $mode The mode to get the personal access token refresh date for.
* @param boolean $raw Whether to return the raw date.
* @return string
*/
public function get_personal_access_token_refresh_date( $mode = 'test', $raw = false ) {
// Personal access tokens are no longer used - return null.
return null;
}
/**
* Get the personal access token should refresh date.
*
* @since 1.8.7
*
* @param string $mode The mode to get the personal access token should refresh date for.
* @param boolean $raw Whether to return the raw date.
* @return string
*/
public function get_personal_access_token_should_refresh( $mode = 'test', $raw = false ) {
// Personal access tokens are no longer used - return false.
return false;
}
/**
* Return the keys to use.
*
* This will return the test keys if test mode is enabled. Otherwise, returns
* the production keys.
*
* @since 1.8.7
*
* @param string $force_mode Forces the "mode" to be used (forcing test for example if the setting is live and vice versa).
* @return string[]
*/
public function get_keys( $force_mode = '' ) {
$keys = array();
$settings = charitable_get_option( 'gateways_square', array() );
if ( charitable_get_option( 'test_mode' ) || 'test' === $force_mode ) {
$keys['access_token'] = trim( $settings['test']['access_token'] );
} else {
$keys['access_token'] = trim( $settings['live']['access_token'] );
}
return $keys;
}
/**
* Get the account ID.
*
* @since 1.8.7
*
* @param string $force_mode The mode to get the account ID for.
* @return string
*/
public function get_account_id( $force_mode = '' ) {
$settings = charitable_get_option( 'gateways_square', array() );
if ( '' === $force_mode ) {
$mode = charitable_get_option( 'test_mode' ) ? 'test' : 'live';
} else {
$mode = $force_mode;
}
return ! empty( $settings[ $mode . '_account_id' ] ) ? $settings[ $mode . '_account_id' ] : '';
}
/**
* Check if Square access token exists.
*
* @since 1.8.7
*
* @param string $mode The mode to check for keys. Should be 'test' or 'live'. If it's sandbox, rename it to 'test'.
* @return boolean
*/
public function check_keys_exist( $mode = '' ) {
if ( '' === $mode ) {
return false;
}
if ( 'sandbox' === $mode ) {
$mode = 'test';
}
$settings = get_option( 'charitable_settings' );
$access_token = $settings['gateways_square'][ $mode . '_access_token' ];
if ( ! empty( $access_token ) ) {
return true;
}
return false;
}
/**
* Return the submitted value for a gateway field.
*
* @since 1.8.7
*
* @param string $key The key of the value we want to get.
* @param mixed[] $values An values in which to search.
* @return string|false
*/
public function get_gateway_value( $key, $values ) {
if ( isset( $values['gateways']['square_core'][ $key ] ) ) {
return $values['gateways']['square_core'][ $key ];
}
return false;
}
/**
* Return the submitted value for a gateway field.
*
* @since 1.8.7
*
* @param string $key The key of the value we want to get.
* @param Charitable_Donation_Processor $processor The Donation Processor helper object.
* @return string|false
*/
public function get_gateway_value_from_processor( $key, Charitable_Donation_Processor $processor ) {
return $this->get_gateway_value( $key, $processor->get_donation_data() );
}
/**
* Returns an array of credit card fields.
*
* If the gateway requires different fields, this can simply be redefined
* in the child class.
*
* @since 1.8.7.
*
* @return array[]
*/
public function get_credit_card_fields() {
$fields = parent::get_credit_card_fields();
$no_wallets = false;
/* Remove all fields. Square will take over */
unset(
$fields['cc_name'],
$fields['cc_number'],
$fields['cc_cvc'],
$fields['cc_expiration']
);
ob_start();
?>
'content',
'content' => ob_get_clean(),
'priority' => 2,
);
return $fields;
}
/**
* Process a donation's payment using the Payment Processor API.
*
* @since 1.8.7
* @since 1.8.8.4
*
* @param array $response The response from the Payment Processor API (Example: "1").
* @param int $donation_id The ID of the donation (Example: "677").
* @param \Charitable_Donation_Processor $processor The Donation Processor object.
* @return boolean|null|array
*/
public function process_donation( $response, $donation_id, \Charitable_Donation_Processor $processor ) {
// CRITICAL DEBUG: Check if this method is being called at all.
if ( charitable_is_debug( 'square' ) ) {
// phpcs:disable
error_log( '=== SQUARE CORE PROCESS_DONATION METHOD CALLED ===' );
error_log( 'Donation ID: ' . $donation_id );
error_log( 'Response: ' . print_r( $response, true ) );
error_log( 'Processor: ' . get_class( $processor ) );
error_log( 'POST data: ' . print_r( $_POST, true ) );
error_log( 'Gateway from processor: ' . $processor->get_donation_data_value( 'gateway' ) );
// phpcs:enable
}
$donation = new \Charitable_Donation( $donation_id );
$mode = charitable_get_option( 'test_mode' ) ? 'test' : 'live';
$connection = Charitable_Square_Connection::get( $mode );
$this->api = new Charitable_Square_API( $connection );
if ( charitable_is_debug( 'square' ) ) {
// phpcs:disable
error_log( 'Mode: ' . $mode );
error_log( 'Connection object: ' . ( $connection ? get_class( $connection ) : 'NULL' ) );
error_log( 'Connection configured: ' . ( $connection ? ( $connection->is_configured() ? 'YES' : 'NO' ) : 'N/A' ) );
error_log( 'Connection valid: ' . ( $connection ? ( $connection->is_valid() ? 'YES' : 'NO' ) : 'N/A' ) );
// phpcs:enable
}
$square_token = isset( $_POST['square_token'] ) ? sanitize_text_field( $_POST['square_token'] ) : ''; // phpcs:ignore
$square_verification_token = isset( $_POST['square_verification_token'] ) ? sanitize_text_field( $_POST['square_verification_token'] ) : ''; // phpcs:ignore
$campaign_name = get_the_title( (int) $_POST['campaign_id'] ); // phpcs:ignore
if ( charitable_is_debug( 'square' ) ) {
// phpcs:disable
error_log( 'Square token: ' . ( $square_token ? 'PRESENT' : 'MISSING' ) );
error_log( 'Square verification token: ' . ( $square_verification_token ? 'PRESENT' : 'MISSING' ) );
error_log( 'Campaign name: ' . $campaign_name );
// phpcs:enable
}
if ( charitable_is_debug( 'square' ) ) {
// phpcs:disable
error_log( 'process_donation - $_POST is ' . print_r( $_POST, true ) );
// phpcs:enable
}
$donation_amount = $this->get_donation_amount_with_fees( $donation );
// Convert to cents — skip for zero-decimal currencies (JPY, KRW,
// VND, etc.) where Square expects the amount in the full unit, not
// 1/100ths. Without this guard, e.g. a ¥1000 donation would be
// submitted as 100000 and the donor charged 100x the intended
// amount.
if ( ! Charitable_Square_Gateway_Processor::is_zero_decimal_currency( $donation->get_currency() ) ) {
$donation_amount = $donation_amount * 100;
}
$args_name = 'Donation ID: ' . $donation_id . ' | ' . ( 'custom' === ! empty( $_POST['donation_amount'] ) ? 'Custom Amount' : 'Suggested Amount' ); // phpcs:ignore
// Set tokens provided by Web Payments SDK.
$this->api->set_payment_tokens( $square_token ); // $entry
if ( charitable_is_debug( 'square' ) ) {
// phpcs:disable
error_log( 'process_donation mode is ' . $mode );
// phpcs:enable
}
$location_id = $this->api->get_location_id( $mode );
$note = get_bloginfo( 'name' ) . ': ' . $campaign_name;
$args_payment_single = array(
'amount' => $donation_amount,
'currency' => charitable_get_currency(),
'location_id' => $location_id,
'note' => $note,
'order_items' => array(
array(
'name' => 'Donations',
'quantity' => 1,
'variations' => array(
array(
'name' => 'Donation',
'quantity' => 1,
'variation_name' => $args_name,
'amount' => $donation_amount,
),
),
),
),
);
$subscription_post_id = $donation->get( 'post_parent' );
if ( $subscription_post_id ) {
if ( charitable_is_debug( 'square' ) ) {
// phpcs:disable
error_log( 'process_donation - subscription_post_id is ' . $subscription_post_id );
// phpcs:enable
}
$subscription = charitable_get_donation( $subscription_post_id );
if ( charitable_is_debug( 'square' ) ) {
// phpcs:disable
// error_log( 'donation is ' . print_r( $donation, true ) );
error_log( 'subscription is ' . print_r( $subscription, true ) );
error_log( 'POST is ' . print_r( $_POST, true ) );
// phpcs:enable
}
$plan = $_POST; //phpcs:ignore
$args_subscription = $this->get_payment_args_subscription( $plan, $campaign_name, $donation_amount );
$args_subscription_single = array(
'amount' => $donation_amount,
'currency' => charitable_get_currency(),
'location_id' => $location_id,
'subscription' => $args_subscription,
);
$result = $this->api->process_subscription_transaction( $args_subscription_single );
if ( charitable_is_debug( 'square' ) ) {
// phpcs:disable
error_log( 'process_donation - result process subscription transaction' );
error_log( print_r( $result, true ) );
// phpcs:enable
}
$subscription_id = $result->getId();
$plan_variation_id = $result->getPlanVariationId();
$customer_id = $result->getCustomerId();
$start_date = $result->getStartDate();
$status = $result->getStatus();
if ( charitable_is_debug( 'square' ) ) {
// phpcs:disable
error_log( 'process_donation - subscription_id is ' . print_r( $subscription_id, true ) );
error_log( 'process_donation - plan_variation_id is ' . print_r( $plan_variation_id, true ) );
error_log( 'process_donation - customer_id is ' . print_r( $customer_id, true ) );
error_log( 'process_donation - start_date is ' . print_r( $start_date, true ) );
error_log( 'process_donation - status is ' . print_r( $status, true ) );
// phpcs:enable
}
$subscription->set_gateway_subscription_id( $subscription_id );
$message = sprintf( '%s: %s', __( 'Square Subscription ID', 'charitable' ), $subscription_id );
$subscription->log()->add( $message );
if ( 'ACTIVE' === $status ) {
$subscription->update_status( 'charitable-active' );
} else {
$subscription->update_status( 'charitable-pending' );
}
if ( charitable_is_debug( 'webhook' ) ) {
// Create a single array of meta data with the remaining items.
$meta_data = array(
'subscription_id' => $subscription_id,
'plan_variation_id' => $plan_variation_id,
'customer_id' => $customer_id,
'start_date' => $start_date,
'status' => $status,
);
update_post_meta( $subscription_post_id, '_square_subscription_meta_data', $meta_data );
}
// We need to update the donation so that we can find it again when the invoice is paid.
update_post_meta( $donation_id, '_square_subscription_donation_first_donation', $subscription_post_id );
if ( charitable_is_debug( 'square' ) ) {
// phpcs:disable
error_log( 'process_donation - donation_id is ' . $donation_id );
error_log( 'process_donation - subscription_post_id is ' . $subscription_post_id );
// phpcs:enable
}
} else {
$result = $this->api->process_single_transaction( $args_payment_single );
if ( charitable_is_debug( 'square' ) ) {
// phpcs:disable
error_log( 'process_donation - result process single transaction' );
error_log( print_r( $result, true ) );
// phpcs:enable
}
// Set payment processing flag.
$this->is_payment_processed = true;
$order = null;
$order_id = '';
$errors = $this->process_api_errors( 'single' );
if ( ! empty( $errors ) ) {
// Process errors and handle categories.
$error_messages = array();
foreach ( $errors as $error ) {
if ( is_array( $error ) && isset( $error['category'] ) ) {
// Handle specific error categories.
switch ( $error['category'] ) {
case 'AUTHENTICATION_ERROR':
$error_messages[] = __( 'Authentication error with Square. Please check your Square Gateway settings.', 'charitable' );
break;
default:
$error_messages[] = $error['message'];
break;
}
} else {
$error_messages[] = $error;
}
}
// If an array is returned, we need to implode it.
$error_message = implode( ', ', $error_messages );
$message = sprintf( '%s: %s', __( 'Errors', 'charitable' ), $error_message );
$donation->log()->add( $message );
// Add a general admin notice to alert the user that there was an error.
$admin_message = sprintf(
'%s' . "\n" . 'Details: %s',
__( 'Errors have occurred while processing Square donations. You may need to check the connection to your Square account.', 'charitable' ),
$error_message
);
if ( charitable_is_debug( 'square' ) ) {
error_log( 'Adding Square admin notice: ' . $admin_message ); //phpcs:ignore
}
// Get current notices.
$current_notices = charitable_get_admin_notices()->get_notices();
$notice_exists = false;
// Check if this exact error message already exists.
if ( ! empty( $current_notices['error'] ) ) {
foreach ( $current_notices['error'] as $notice ) {
if ( $notice['message'] === $admin_message ) {
$notice_exists = true;
break;
}
}
}
// Only add the notice if it doesn't already exist.
if ( ! $notice_exists ) {
charitable_get_admin_notices()->add_notice( $admin_message, 'error', false, true );
}
if ( charitable_is_debug( 'square' ) ) {
error_log( 'Current admin notices: ' . print_r( charitable_get_admin_notices()->get_notices(), true ) ); //phpcs:ignore
}
$donation->update_status( 'charitable-failed' );
wp_send_json_error( $admin_message );
exit;
}
if ( isset( $result['order'] ) ) {
$order = $result['order'];
$order_id = $order->getId();
}
if ( $order_id ) {
// This Square Order / Transaction ID is used by the webhook to update the donation status.
$donation->set_gateway_transaction_id( $order_id );
$message = sprintf( '%s: %s', __( 'Square Order / Transaction ID', 'charitable' ), $order_id );
$donation->log()->add( $message );
}
}
if ( charitable_is_debug( 'square' ) ) {
// phpcs:disable
error_log( 'process_donation - end' );
// phpcs:enable
}
$donation->update_status( 'charitable-pending' );
return true;
}
/**
* Retrieve subscription payment args.
*
* @since 1.8.7
*
* @param array $plan Plan settings.
* @param string $campaign_name Campaign name.
* @param int $donation_amount Donation amount.
*
* @return array
*/
private function get_payment_args_subscription( array $plan, string $campaign_name, int $donation_amount ): array {
$args_sub = array();
$currency = charitable_get_currency();
$recurring_donation_plan = ! empty( $plan['recurring_donation_plan'] ) ? $plan['recurring_donation_plan'] : '';
$recurring_donation_plan = empty( $recurring_donation_plan ) && ! empty( $plan['recurring_donation'] ) ? sanitize_text_field( $plan['recurring_donation'] ) : $recurring_donation_plan;
$recurring_donation_plan = apply_filters( 'charitable_square_recurring_donation_plan', $recurring_donation_plan, $plan );
switch ( $recurring_donation_plan ) {
case 'day':
case 'daily':
$phase_cadence_key = 'daily';
break;
case 'week':
case 'weekly':
$phase_cadence_key = 'weekly';
break;
case 'month':
case 'monthly':
$phase_cadence_key = 'monthly';
break;
case 'quarter':
case 'quarterly':
$phase_cadence_key = 'quarterly';
break;
case 'semiyear':
case 'semiyearly':
$phase_cadence_key = 'semiyearly';
break;
case 'year':
case 'yearly':
case 'annually':
case 'annual':
$phase_cadence_key = 'yearly';
break;
}
$plan['name'] = get_bloginfo( 'name' );
$args_sub['customer']['email'] = sanitize_email( $_POST['email'] ); //phpcs:ignore
$args_sub['customer']['first_name'] = sanitize_text_field( $_POST['first_name'] ); //phpcs:ignore
$args_sub['customer']['last_name'] = sanitize_text_field( $_POST['last_name'] ); //phpcs:ignore
// Customer address.
// There's address_2, city, state, postcode, country.
$args_sub['customer']['address'] = ! empty( $_POST['address'] ) ? sanitize_text_field( $_POST['address'] ) : ''; //phpcs:ignore
$args_sub['customer']['address_2'] = ! empty( $_POST['address_2'] ) ? sanitize_text_field( $_POST['address_2'] ) : ''; //phpcs:ignore
$args_sub['customer']['city'] = ! empty( $_POST['city'] ) ? sanitize_text_field( $_POST['city'] ) : ''; //phpcs:ignore
$args_sub['customer']['state'] = ! empty( $_POST['state'] ) ? sanitize_text_field( $_POST['state'] ) : ''; //phpcs:ignore
$args_sub['customer']['postcode'] = ! empty( $_POST['postcode'] ) ? sanitize_text_field( $_POST['postcode'] ) : ''; //phpcs:ignore
$args_sub['customer']['country'] = ! empty( $_POST['country'] ) ? sanitize_text_field( $_POST['country'] ) : ''; //phpcs:ignore
$args_sub['customer']['phone'] = ! empty( $_POST['phone'] ) ? sanitize_text_field( $_POST['phone'] ) : ''; //phpcs:ignore
$cadences_list = Charitable_Square_Helpers::get_subscription_cadences();
if ( charitable_is_debug( 'square' ) ) {
// phpcs:disable
error_log( 'cadences_list is ' . print_r( $cadences_list, true ) );
// phpcs:enable
}
$phase_cadence = $cadences_list[ $phase_cadence_key ] ?? $cadences_list['yearly'];
// Subscription cadence.
$args_sub['phase_cadence'] = $phase_cadence;
$plan_name = $campaign_name;
$plan_name .= empty( $plan['name'] ) ? '' : ': ' . $plan['name'];
$args_sub['plan_name'] = sprintf( '%s (%s)', $plan_name, $phase_cadence['name'] );
$args_sub['plan_variation_name'] = sprintf( '%s (%s %s %s)', $plan['name'], $donation_amount, $currency, $phase_cadence['name'] );
// Card holder.
$args_sub['card_name'] = sanitize_text_field( $_POST['first_name'] ) . ' ' . sanitize_text_field( $_POST['last_name'] ); // phpcs:ignore
if ( charitable_is_debug( 'square' ) ) {
// phpcs:disable
error_log( 'get_payment_args_subscription - args_sub is ' . print_r( $args_sub, true ) );
// phpcs:enable
}
/**
* Filter subscription payment arguments.
*
* @since 1.8.7
*
* @param array $args The subscription payment arguments.
* @param Process $process The Process instance.
*/
return (array) apply_filters( 'square_integrations_square_process_get_payment_args_subscription', $args_sub, $this ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- This is a third-party hook from the Square Integrations plugin. Changing it would break existing functionality.
}
/**
* Collect errors from API and turn it into form errors.
*
* @since 1.8.7
*
* @param string $type Payment type (e.g. 'single').
*/
private function process_api_errors( string $type ) {
$errors = $this->api->get_errors();
if ( empty( $errors ) || ! is_array( $errors ) ) {
return;
}
$this->log_errors( $errors );
if ( $type === 'subscription' ) {
$title = esc_html__( 'Square subscription payment stopped', 'charitable' );
} else {
$title = esc_html__( 'Square payment stopped', 'charitable' );
}
$_errors = $this->api->get_response_errors();
if ( ! empty( $_errors ) ) {
$this->process_api_errors_codes( $_errors );
$errors[] = $_errors;
}
// Process errors and extract categories.
$processed_errors = array();
foreach ( $errors as $error ) {
if ( is_array( $error ) && isset( $error['category'] ) ) {
// Handle error with category.
$processed_errors[] = array(
'message' => $error['message'],
'category' => $error['category'],
);
} else {
// Handle regular error string.
$processed_errors[] = $error;
}
}
if ( ! empty( $processed_errors ) && charitable_is_debug( 'square' ) ) {
// phpcs:disable
error_log( 'Charitable_Gateway_Square. Errors reported in API. ' . print_r( $processed_errors, true ) );
// phpcs:enable
}
return $processed_errors;
}
/**
* Display form errors.
*
* @since 1.8.7
*
* @param array $errors Errors to display.
*/
private function log_errors( array $errors = array() ) {
if ( ! $errors ) {
$errors = $this->errors;
}
if ( ! $errors || ! is_array( $errors ) ) {
return;
}
if ( charitable_is_debug( 'square' ) ) {
// phpcs:disable
error_log( 'Charitable_Gateway_Square. Errors replorted in API. ' . print_r( $errors, true ) );
// phpcs:enable
}
}
/**
* Check specific error codes.
*
* @since 1.8.7
*
* @param array $errors The last API call errors.
*/
private function process_api_errors_codes( array $errors ) {
$codes = $this->get_oauth_error_codes();
foreach ( $errors as $error ) {
if (
empty( $error['code'] ) ||
! in_array( $error['code'], $codes, true )
) {
continue;
}
// If the error indicates that access token is bad, set a connection as invalid.
$this->connection
->set_status( Charitable_Square_Connection::STATUS_INVALID )
->save();
}
}
/**
* Retrieve OAuth-related errors.
* used to be return [ ErrorCode::ACCESS_TOKEN_EXPIRED, ErrorCode::ACCESS_TOKEN_REVOKED, ErrorCode::UNAUTHORIZED ];
*
* @since 1.8.7
*
* @link https://developer.squareup.com/docs/oauth-api/best-practices#ensure-api-calls-made-with-oauth-tokens-handle-token-based-errors-appropriately
*
* @return array
*/
private function get_oauth_error_codes(): array {
return array( 'ACCESS_TOKEN_EXPIRED', 'ACCESS_TOKEN_REVOKED', 'UNAUTHORIZED' );
}
/**
* Validate the submitted credit card details.
*
* @since 1.8.7
* @since 1.4.0 Deprecated.
* @since 1.4.7 Restored. No longer deprecated.
*
* @param boolean $valid Whether the donation is valid.
* @param string $gateway The chosen gateway.
* @param mixed[] $values The filtered values from the donation form submission.
* @return boolean
*/
public static function validate_donation( $valid, $gateway, $values ) {
if ( 'square_core' !== $gateway ) {
return $valid;
}
if ( charitable_is_debug( 'square' ) ) {
// phpcs:disable
error_log( 'validate_donation : Charitable_Gateway_Square is making an attempt to validate the donation' );
error_log( '$valid: ' . $valid );
error_log( '$gateway: ' . $gateway );
error_log( '$values: ' . print_r( $values, true ) );
// phpcs:enable
}
$gateway = new Charitable_Gateway_Square();
$test_mode = charitable_get_option( 'test_mode' ) ? 'test' : 'live';
if ( empty( charitable_square_get_access_token( $test_mode ) ) ) {
charitable_get_notices()->add_error( __( 'Missing access token for Square payment gateway. Unable to proceed with payment.', 'charitable' ) );
return false;
}
return $valid;
}
/**
* Check whether a particular donation can be refunded automatically in Square.
*
* @since 1.8.7
*
* @param Charitable_Donation $donation The donation object.
* @return boolean
*/
public function is_donation_refundable( Charitable_Donation $donation ) {
$mode = charitable_get_option( 'test_mode' ) ? 'test' : 'live';
$connection = Charitable_Square_Connection::get( $mode );
if ( ! $connection || ! $connection->is_configured() ) {
$this->errors[] = esc_html__( 'Square account connection is missing.', 'charitable' );
return false;
}
if ( ! $connection->is_valid() ) {
$this->errors[] = esc_html__( 'Square account connection is invalid.', 'charitable' );
return false;
}
if ( ! $connection->is_currency_matched() ) {
$this->errors[] = esc_html__( 'The currency associated with the payment is not valid for the provided business location.', 'charitable' );
return false;
}
// We need to get the payment id from the donation.
$payment_id = $donation->get_gateway_payment_id();
if ( ! $payment_id ) {
return false;
}
return true;
}
/**
* Process a refund initiated in the WordPress dashboard.
*
* @since 1.8.7
* @since 1.8.8.4
*
* @param int $donation_id The donation ID.
* @return boolean
*/
public static function refund_donation_from_dashboard( $donation_id ) {
// Debug: Log that the Square Core refund method is being called.
if ( charitable_is_debug( 'square' ) ) {
// phpcs:disable
error_log( '[Square Core] Refund method called for donation ID: ' . $donation_id );
// phpcs:enable
}
$donation = charitable_get_donation( $donation_id );
if ( ! $donation ) {
if ( charitable_is_debug( 'square' ) ) {
// phpcs:disable
error_log( '[Square Core] Refund failed - donation not found for ID: ' . $donation_id );
// phpcs:enable
}
return false;
}
try {
$mode = charitable_get_option( 'test_mode' ) ? 'test' : 'live';
$connection = Charitable_Square_Connection::get( $mode );
$api = new Charitable_Square_API( $connection );
$payment_id = $donation->get_gateway_payment_id();
if ( ! $payment_id ) {
return false;
}
$currency = $donation->get_currency();
$amount = self::get_donation_amount_with_fees_static( $donation );
// Convert amount to cents — skip for zero-decimal currencies
// (JPY, KRW, VND, etc.).
$amount_cents = Charitable_Square_Gateway_Processor::is_zero_decimal_currency( $currency )
? $amount
: $amount * 100;
$args = array(
'reason' => 'Refunded from the WordPress dashboard',
'amount' => $amount_cents,
'currency' => $currency,
);
$refund = $api->refund_payment( $payment_id, $args );
if ( ! $refund ) {
// Log the failed refund.
$donation->log()->add(
sprintf(
/* translators: %s: error message. */
__( 'Square refund failed: %s', 'charitable' ),
$api->get_errors()
)
);
return false;
}
// Log the refund.
$donation->log()->add(
sprintf(
/* translators: %s: transaction reference. */
__( 'Square refund successful. Refund transaction ID: %s', 'charitable' ),
$refund->getId()
)
);
} catch ( Exception $e ) {
$donation->log()->add(
sprintf(
/* translators: %s: error message. */
__( 'Square refund failed: %s', 'charitable' ),
$e->getMessage()
)
);
return false;
}
}
/**
* Get the donation amount including fees if fee relief is enabled.
*
* @since 1.8.8.4
*
* @param Charitable_Donation $donation The donation object.
* @return float
*/
private static function get_donation_amount_with_fees_static( $donation ) {
if ( ! $donation ) {
return 0.0;
}
// Check if fee relief is enabled and donor opted to cover fees.
if ( $donation->get( 'cover_fees' ) ) {
return \Charitable_Currency::get_instance()->cast_to_decimal_format( $donation->get( 'total_donation_with_fees' ) );
}
// Return the standard donation amount.
return (float) $donation->get_total_donation_amount( true );
}
/**
* Cancel a subscription.
*
* To cancel a subscription, you need to:
* Create a CancelSubscriptionsRequest object
* Pass it to the subscriptions->cancel() method
* Handle the response to determine success or failure
*
* @since 1.8.7
*
* @param boolean $cancelled Whether the subscription was cancelled successfully in the gateway.
* @param Charitable_Recurring_Donation $donation The recurring donation object.
* @return boolean
*/
public static function cancel_subscription( $cancelled, $donation ) {
$subscription_id = $donation->get_gateway_subscription_id();
if ( charitable_is_debug( 'square' ) ) {
// phpcs:disable
error_log( 'cancel_subscription - subscription_id: ' . $subscription_id );
// phpcs:enable
}
if ( ! $subscription_id ) {
return false;
}
$mode = charitable_get_option( 'test_mode' ) ? 'test' : 'live';
$connection = Charitable_Square_Connection::get( $mode );
$api = new Charitable_Square_API( $connection );
$result = $api->process_subscription_cancellation( $subscription_id );
if ( charitable_is_debug( 'square' ) ) {
// phpcs:disable
error_log( 'cancel_subscription - result: ' . $result );
// phpcs:enable
}
if ( $result ) {
$donation->log()->add( __( 'Subscription initialised for cancellation in Square.', 'charitable' ) );
update_post_meta( $donation->ID, '_square_cancelled_to_be_confirmed', 'true' );
return true;
} else {
$errors = $api->get_errors();
$message = ! empty( $errors ) ? implode( ', ', $errors ) : __( 'Unknown error.', 'charitable' );
$donation->log()->add(
sprintf(
/* translators: %s: error message. */
__( 'Square cancellation failed: %s', 'charitable' ),
$message
)
);
return false;
}
}
/**
* Sets up the API.
*
* This sets the API key, specifies an API version to use, and also
* sets the App info.
*
* This function is deprecated.
*
* @since 1.8.7
*
* @param string|null $api_key The API key. Might be the access token. (Maybe later: If null, will use the secret key).
* @return boolean
*/
public function setup_api( $api_key = null ) {
return true;
}
/**
* Determine if the Square addon should be shown on the frontend if no keys are found
*
* @param array[] $active_gateways The list of registered gateways.
* @return string[]
* @since 1.8.7
* @version 1.8.7 - not currently used.
*/
public function maybe_active_public_gateway( $active_gateways ) {
if ( ! isset( $active_gateways['square_core'] ) ) {
return $active_gateways;
}
if ( is_admin() ) {
// if this is the admin backend don't mess with this list.
return $active_gateways;
}
global $post;
if ( false === $post || ! isset( $post->ID ) ) {
return $active_gateways;
}
// ok, this is being loaded as an active gateway - but do we have any keys?
$keys = $this->get_keys();
/* Make sure that the keys are set, otherwise there will likely be JS error in the template that will effect other JS generated by the plugin on the page. */
if ( empty( $keys['secret_key'] ) || empty( $keys['public_key'] ) ) {
charitable_get_notices()->add_error( __( 'Missing keys for Square payment gateway. Unable to proceed with payment.', 'charitable' ) );
unset( $active_gateways['square_core'] );
}
return $active_gateways;
}
/**
* Enqueue Square Web Payments SDK scripts.
* Only load on donation forms.
*
* @since 1.8.7
*
* @return void
*/
public function enqueue_square_scripts() {
// Check if Square Core is active before loading scripts.
if ( ! self::is_square_core_active() ) {
if ( charitable_is_debug( 'square' ) ) {
// phpcs:disable
error_log( '[Charitable Square Core] Scripts not loaded - Square Core is not active' );
// phpcs:enable
}
return;
}
if ( charitable_is_debug( 'square' ) ) {
// phpcs:disable
error_log( '[Charitable Square Core] Loading Square Core scripts - gateway is active' );
// phpcs:enable
}
if ( ! is_admin() ) {
$connection = Charitable_Square_Connection::get();
if ( ! $connection || ! $connection->is_usable() ) {
return;
}
// Get current mode and debug information.
$mode = charitable_get_option( 'test_mode' ) ? 'test' : 'live';
// Ensure the JavaScript reference in your HTML is pointing to the correct domain.
$script_url = $mode === 'test' ? 'https://sandbox.web.squarecdn.com/v1/square.js' : 'https://web.squarecdn.com/v1/square.js';
/* Register Square JS script. */
wp_register_script(
'square-payments-sdk',
$script_url,
array(),
'1.0.0',
true
);
$dependencies = array( 'charitable-script', 'square-payments-sdk', 'jquery' );
wp_register_script(
'charitable',
charitable()->get_path( 'assets', false ) . 'js/square/charitable-square.js',
$dependencies,
'1.0.0',
true
);
/**
* This filter allows to set a card configuration and styles.
*
* @since 1.8.7.
* @version 1.8.7 removed 'billing_details' => $this->get_mapped_contact_fields( $forms ),
*
* @param array $card_config Configuration and style options.
*/
$card_config = (array) apply_filters( 'charitable_square_frontend_enqueues_card_config', array() );
wp_localize_script(
'charitable',
'CHARITABLE_SQUARE_VARS',
array(
'client_id' => $connection->get_client_id(),
'location_id' => Charitable_Square_Helpers::get_location_id(),
'card_config' => $card_config,
'enable_apple_pay' => false,
'enable_google_pay' => false,
'i18n' => array(
'missing_sdk_script' => esc_html__( 'Square.js failed to load properly.', 'charitable' ),
'general_error' => esc_html__( 'An unexpected Square SDK error has occurred.', 'charitable' ),
'missing_creds' => esc_html__( 'Client ID and/or Location ID is incorrect.', 'charitable' ),
'card_init_error' => esc_html__( 'Initializing Card failed.', 'charitable' ),
'token_process_fail' => esc_html__( 'Tokenization of the payment card failed.', 'charitable' ),
'token_status_error' => esc_html__( 'Tokenization failed with status:', 'charitable' ),
'buyer_verify_error' => esc_html__( 'The verification was not successful. An issue occurred while verifying the buyer.', 'charitable' ),
'empty_details' => esc_html__( 'Please fill out payment details to continue.', 'charitable' ),
'wallet_not_supported' => esc_html__( 'Recurring payments can\'t be processed using Google Pay or Apple Pay. Please enter your card details.', 'charitable' ),
),
)
);
wp_enqueue_script( 'charitable' );
// Debug information.
if ( charitable_is_debug( 'square' ) ) {
// phpcs:disable
error_log( 'Test / Square Mode: ' . $mode );
error_log( 'Application ID: ' . $this->get_square_application_id( $mode ) );
// phpcs:enable
}
}
}
/**
* Get the Square application ID.
*
* @since 1.8.7
*
* @param string $mode The mode to get the application ID for (test or live).
* @return string
*/
private function get_square_application_id( $mode = 'test' ) {
if ( 'sandbox' === $mode ) {
$mode = 'test';
}
$option_value = charitable_get_option( array( 'gateways_square', $mode, 'application_id' ) );
return $option_value;
}
/**
* Get the Square location ID.
* Might be deprecated.
*
* @since 1.8.7
*
* @param string $mode The mode to get the location ID for (test or live).
* @return string
*/
private function get_square_location_id( $mode = 'test' ) {
if ( empty( $mode ) ) {
$mode = charitable_get_option( 'test_mode' ) ? 'test' : 'live';
}
if ( 'sandbox' === $mode ) {
$mode = 'test';
}
$option_value = charitable_get_option( array( 'gateways_square', $mode, 'location' ) );
return $option_value;
}
/**
* Retrieve the Disconnect button.
*
* @since 1.8.7
*
* @param string $mode Square mode.
* @param bool $wrap Optional. Wrap a button HTML element or not.
*
* @return string
*/
private function get_disconnect_button( string $mode, bool $wrap = true ): string {
$button = sprintf(
'
%3$s ',
esc_url( $this->connect->get_disconnect_url( $mode ) ),
esc_attr__( 'Disconnect Square account', 'charitable' ),
esc_html__( 'Disconnect', 'charitable' )
);
return $wrap ? '
' . $button . '
' : $button;
}
/**
* Retrieve the Refresh tokens button.
*
* @since 1.8.7
*
* @param string $mode Square mode.
*
* @return string
*/
private function get_refresh_button( string $mode ): string {
return sprintf(
'
%4$s ',
esc_attr( $mode ),
esc_url( $this->get_settings_page_url() ),
esc_attr__( 'Refresh connection tokens', 'charitable' ),
esc_html__( 'Refresh tokens', 'charitable' )
);
}
/**
* Retrieve the Error icon emoji.
*
* @since 1.8.7
*
* @return string
*/
private function get_error_icon(): string {
return '
';
}
/**
* Get the settings page URL.
*
* @since 1.8.7
*
* @return string
*/
public function get_settings_page_url() {
return add_query_arg(
array(
'page' => 'charitable-settings',
'tab' => 'gateways',
'group' => 'gateways_square_core',
),
admin_url( 'admin.php' )
);
}
/**
* Create instance of the class.
*
* @since 1.8.7
*
* @return Charitable_Gateway_Square
*/
public static function get_instance() {
if ( null === self::$instance ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Check the Square token status and display admin notice if needed.
*
* @since 1.8.7
*/
public function check_token_status() {
global $wpdb;
// Only run on Square settings page.
if ( ! is_admin() ) {
return;
}
// If the error notice is already set, do not run this again.
$option = get_option( 'charitable_square_connection_error_notice' );
if ( ! empty( $option ) ) {
// If the error notice is already set, do not run this again.
if ( charitable_is_debug( 'square' ) ) {
// phpcs:disable
error_log( 'Square token status check already performed. Skipping.' );
// phpcs:enable
}
return;
}
$mode = charitable_get_option( 'test_mode' ) ? 'test' : 'live';
$connection = Charitable_Square_Connection::get( $mode );
$api = new Charitable_Square_API( $connection );
if ( isset( $_GET['test_delete'] ) ) { //phpcs:ignore
$status['is_valid'] = '';
$this->generate_admin_notice_for_square_connection_error( __( 'Your Square connection has expired or been revoked. Please reconnect to continue accepting donations.', 'charitable' ), 'square_connection_error' );
}
// Only check if we have a configured connection.
if ( ! $connection || ! $connection->is_configured() ) {
return;
}
$status = $api->check_token_status();
if ( charitable_is_debug( 'square' ) ) {
error_log( 'Square token status check: ' . print_r( $status, true ) ); //phpcs:ignore
}
if ( ! $status['is_valid'] ) {
if ( charitable_is_debug( 'square' ) ) {
error_log( 'Square token status check failed: ' . $status['message'] ); //phpcs:ignore
}
if ( $connection ) {
// Delete the transient to ensure we can recreate the error message.
$connections = (array) get_option( 'charitable_square_connections', array() );
unset( $connections[ $mode ] );
empty( $connections ) ? delete_option( 'charitable_square_connections' ) : update_option( 'charitable_square_connections', $connections );
$settings = get_option( 'charitable_settings' );
$settings['gateways_square'][ $mode ] = array();
$data = array(
'option_value' => maybe_serialize( $settings ),
);
$where = array(
'option_name' => 'charitable_settings',
);
$ret = $wpdb->update( //phpcs:ignore
$wpdb->options,
$data,
$where,
array( '%s' ),
array( '%s' )
);
}
// Are in live mode (production)?
if ( ! charitable_get_option( 'test_mode' ) ) {
Charitable_Square_Connect::get_instance()->unschedule_refresh();
}
// Webhooks are now manually configured in Square dashboard - no need to reset.
// Charitable_Square_Helpers::reset_webhook_configuration( true );.
// Not needed because this as stored in the "connection" that was wiped above.
$api->set_location_id( '', $mode );
$api->set_location_currency( '', $mode );
$api->detete_transients( $mode );
// Use a regex to extract the value between the quotes after "detail".
preg_match( '/"detail":"([^"]+)"/', $status['message'], $matches );
$message = isset( $matches[1] ) ? $matches[1] : '';
// Get the Status code.
preg_match( '/Status code: (\d+)/', $status['message'], $matches );
$status_code = isset( $matches[1] ) ? $matches[1] : '';
$this->generate_admin_notice_for_square_connection_error( $message, $status_code, 'square_connection_error' );
// Do not force-redirect the admin from here. This method runs on
// every admin_init, so redirecting hijacked unrelated pages and
// looped whenever the dismissible notice flag was cleared. The
// admin notice already prompts the reconnect.
}
}
/**
* Generate admin notice for Square connection error.
*
* @since 1.8.7
*
* @param string $message The error message.
* @param string $status_code The status code.
* @param string $notice_key The notice key.
*/
private function generate_admin_notice_for_square_connection_error( $message = '', $status_code = '', $notice_key = '' ) {
// Store the notice in the database.
$admin_notice = array(
'message' => $message,
'status_code' => $status_code,
'time_added' => time(),
);
update_option( 'charitable_square_connection_error_notice', $admin_notice );
}
/**
* Check if this is the Square settings page.
*
* @since 1.8.7
*
* @return boolean
*/
private function is_square_settings_page() {
return charitable_is_admin_screen( 'settings' ) && isset( $_GET['group'] ) && 'gateways_square_core' === $_GET['group']; //phpcs:ignore
}
/**
* Check if we should clear the mode connection warning.
*
* @since 1.8.5
*
* @param array $values The values being saved.
* @return void
*/
private function maybe_clear_mode_connection_warning( $values ) {
$warning = get_option( 'charitable_square_mode_connection_warning' );
if ( ! $warning ) {
return;
}
$mode = $warning['mode'];
// Check if there's now a valid connection for the mode that had the warning.
if ( ! empty( $values['gateways_square'][ $mode ]['access_token'] ) ) {
$connection = Charitable_Square_Connection::get( $mode );
if ( $connection && $connection->is_configured() && $connection->is_valid() ) {
// Clear the warning since we now have a valid connection.
delete_option( 'charitable_square_mode_connection_warning' );
}
}
}
/**
* Check if Square Core gateway is active.
*
* @since 1.8.7
*
* @return boolean
*/
public static function is_square_core_active() {
$active_gateways = charitable_get_helper( 'gateways' )->get_active_gateways();
$is_active = isset( $active_gateways['square_core'] );
if ( charitable_is_debug( 'square' ) ) {
// phpcs:disable
error_log( '[Charitable Square Core] Checking if Square Core is active: ' . ( $is_active ? 'true' : 'false' ) );
// phpcs:enable
}
return $is_active;
}
/**
* Check if Square Legacy gateway is active.
*
* @since 1.8.7
*
* @return boolean
*/
public static function is_square_legacy_active() {
$active_gateways = charitable_get_helper( 'gateways' )->get_active_gateways();
$is_active = isset( $active_gateways['square'] );
if ( charitable_is_debug( 'square' ) ) {
// phpcs:disable
error_log( '[Charitable Square Core] Checking if Square Legacy is active: ' . ( $is_active ? 'true' : 'false' ) );
// phpcs:enable
}
return $is_active;
}
/**
* Get the donation amount including fees if fee relief is enabled.
*
* @since 1.8.8.3
*
* @param Charitable_Donation $donation The donation object.
* @return float
*/
private function get_donation_amount_with_fees( $donation ) {
// Check if fee relief is enabled and donor opted to cover fees
if ( $donation->get( 'cover_fees' ) ) {
// Use the total donation amount including fees
return \Charitable_Currency::get_instance()->cast_to_decimal_format( $donation->get( 'total_donation_with_fees' ) );
}
// Return the standard donation amount
return $donation->get_total_donation_amount( true );
}
}
endif;