PluginProbe
SureDonation – Donation Forms, Fundraising Campaigns & Donor Management / 1.1.2
SureDonation – Donation Forms, Fundraising Campaigns & Donor Management v1.1.2
1.6.0 1.5.1 1.5.0 1.4.0 1.3.0 trunk 0.0.1 1.0.0 1.1.0 1.1.1 1.1.2 1.2.0
suredonation / inc / api / onboarding-api.php

onboarding-api.php in SureDonation – Donation Forms, Fundraising Campaigns & Donor Management 1.1.2, at inc/api/onboarding-api.php

316 lines 8.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Onboarding REST endpoints.
4 *
5 * Routes (under `suredonation/v1`):
6 * - GET /onboarding/get-status — return { completed: 'yes'|'no' }
7 * - POST /onboarding/set-status — write completion + optional analytics
8 * - POST /onboarding/create-campaign — create a draft suredonation_cmpgn
9 * - POST /onboarding/user-details — persist lead capture (free-only step)
10 *
11 * @package SureDonation
12 */
13
14 namespace SureDonation\Inc\API;
15
16 use SureDonation\Inc\Campaigns\Campaign_Cpt;
17 use SureDonation\Inc\Helper;
18 use SureDonation\Inc\Onboarding;
19 use WP_Error;
20 use WP_REST_Request;
21 use WP_REST_Response;
22 use WP_REST_Server;
23
24 if ( ! defined( 'ABSPATH' ) ) {
25 exit;
26 }
27
28 /**
29 * Onboarding REST API endpoints.
30 *
31 * @since 1.0.0
32 */
33 class Onboarding_API {
34 /**
35 * Allowed goal types for the create-campaign endpoint.
36 *
37 * @since 1.0.0
38 * @var array<int,string>
39 */
40 private const GOAL_TYPES = [ 'raised_amount', 'donation_count' ];
41
42 /**
43 * Return endpoint definitions for Rest_Api to register.
44 *
45 * @return array<string,mixed>
46 * @since 1.0.0
47 */
48 public function get_endpoints() {
49 return [
50 '/onboarding/get-status' => [
51 'methods' => WP_REST_Server::READABLE,
52 'callback' => [ $this, 'get_status' ],
53 'permission_callback' => [ $this, 'check_permissions' ],
54 ],
55 '/onboarding/set-status' => [
56 'methods' => WP_REST_Server::EDITABLE,
57 'callback' => [ $this, 'set_status' ],
58 'permission_callback' => [ $this, 'check_permissions' ],
59 ],
60 '/onboarding/create-campaign' => [
61 'methods' => WP_REST_Server::EDITABLE,
62 'callback' => [ $this, 'create_campaign' ],
63 'permission_callback' => [ $this, 'check_permissions' ],
64 ],
65 '/onboarding/user-details' => [
66 'methods' => WP_REST_Server::EDITABLE,
67 'callback' => [ $this, 'save_user_details' ],
68 'permission_callback' => [ $this, 'check_permissions' ],
69 ],
70 ];
71 }
72
73 /**
74 * Permission gate.
75 *
76 * @return bool
77 * @since 1.0.0
78 */
79 public function check_permissions() {
80 return current_user_can( 'manage_options' );
81 }
82
83 /**
84 * GET /onboarding/get-status.
85 *
86 * @return WP_REST_Response
87 * @since 1.0.0
88 */
89 public function get_status() {
90 return new WP_REST_Response(
91 [
92 'completed' => Onboarding::get_instance()->is_completed() ? 'yes' : 'no',
93 ]
94 );
95 }
96
97 /**
98 * POST /onboarding/set-status.
99 *
100 * @param WP_REST_Request $request Request.
101 * @return WP_REST_Response
102 * @since 1.0.0
103 */
104 public function set_status( $request ) {
105 $completed = $request->get_param( 'completed' );
106 Onboarding::get_instance()->set_completed( 'yes' === $completed ? 'yes' : 'no' );
107
108 return new WP_REST_Response( [ 'success' => true ] );
109 }
110
111 /**
112 * POST /onboarding/create-campaign.
113 *
114 * Creates a draft campaign post + writes its meta. Returns the new
115 * campaign id + edit URL so the JS can persist it in onboarding state.
116 *
117 * @param WP_REST_Request $request Request.
118 * @return WP_REST_Response|WP_Error
119 * @since 1.0.0
120 */
121 public function create_campaign( $request ) {
122 $name = sanitize_text_field( (string) $request->get_param( 'name' ) );
123 $goal_type = (string) $request->get_param( 'goal_type' );
124 $description = wp_kses_post( (string) $request->get_param( 'description' ) );
125
126 // Clamp to a non-negative, finite, sane range. The JS already
127 // validates this, but the endpoint is callable directly by any
128 // manage_options user and shouldn't trust client-side bounds.
129 $goal_amount = (float) $request->get_param( 'goal_amount' );
130 if ( ! is_finite( $goal_amount ) || $goal_amount < 0 ) {
131 $goal_amount = 0.0;
132 }
133 // Cap at 1e9 so a stray "1e308" can't poison campaign meta.
134 $goal_amount = min( $goal_amount, 1000000000.0 );
135
136 if ( '' === trim( $name ) ) {
137 return new WP_Error(
138 'suredonation_campaign_name_required',
139 __( 'Campaign name is required.', 'suredonation' ),
140 [ 'status' => 400 ]
141 );
142 }
143
144 if ( ! in_array( $goal_type, self::GOAL_TYPES, true ) ) {
145 $goal_type = 'raised_amount';
146 }
147
148 $result = wp_insert_post(
149 [
150 'post_type' => Campaign_Cpt::POST_TYPE,
151 'post_status' => 'draft',
152 'post_title' => $name,
153 'post_excerpt' => $description,
154 'post_author' => get_current_user_id(),
155 ],
156 true
157 );
158
159 if ( is_wp_error( $result ) ) {
160 return new WP_Error(
161 'suredonation_campaign_create_failed',
162 $result->get_error_message(),
163 [ 'status' => 500 ]
164 );
165 }
166
167 $campaign_id = (int) $result;
168
169 if ( $campaign_id <= 0 ) {
170 return new WP_Error(
171 'suredonation_campaign_create_failed',
172 __( 'Could not create the campaign.', 'suredonation' ),
173 [ 'status' => 500 ]
174 );
175 }
176
177 Helper::update_campaign_meta(
178 $campaign_id,
179 [
180 'goal_type' => $goal_type,
181 'goal_amount' => $goal_amount,
182 ]
183 );
184
185 return new WP_REST_Response(
186 [
187 'success' => true,
188 'campaign_id' => $campaign_id,
189 'edit_url' => admin_url( 'admin.php?page=suredonation#/campaigns/' . $campaign_id ),
190 ]
191 );
192 }
193
194 /**
195 * POST /onboarding/user-details.
196 *
197 * Stores the lead-capture payload under suredonation_options so we
198 * don't re-prompt on subsequent setup passes.
199 *
200 * @param WP_REST_Request $request Request.
201 * @return WP_REST_Response
202 * @since 1.0.0
203 */
204 public function save_user_details( $request ) {
205 $onboarding = Onboarding::get_instance();
206
207 $payload = [
208 'first_name' => sanitize_text_field( (string) $request->get_param( 'first_name' ) ),
209 'last_name' => sanitize_text_field( (string) $request->get_param( 'last_name' ) ),
210 'email' => sanitize_email( (string) $request->get_param( 'email' ) ),
211 'opted_in' => (bool) $request->get_param( 'opted_in' ),
212 ];
213
214 $onboarding->set_user_details( $payload );
215
216 update_site_option(
217 'suredonation_usage_optin',
218 $payload['opted_in'] ? 'yes' : 'no'
219 );
220
221 if ( ! $onboarding->is_lead_sent() && $this->forward_lead_to_crm( $payload ) ) {
222 $onboarding->mark_lead_sent();
223 }
224
225 /**
226 * Fires after onboarding lead-capture details are persisted.
227 *
228 * @since 1.0.0
229 *
230 * @param array<string,mixed> $payload Sanitised payload.
231 */
232 do_action( 'suredonation_onboarding_user_details_saved', $payload );
233
234 return new WP_REST_Response( [ 'success' => true ] );
235 }
236
237 /**
238 * Generate lead.
239 *
240 * @param array<string,mixed> $payload Sanitised lead-capture payload.
241 * @return bool True when the CRM accepted the lead, false otherwise.
242 * @since 1.1.2
243 */
244 private function forward_lead_to_crm( array $payload ) {
245 $email_raw = $payload['email'] ?? '';
246 $email = is_string( $email_raw ) ? sanitize_email( $email_raw ) : '';
247 if ( empty( $email ) || ! is_email( $email ) ) {
248 return false;
249 }
250
251 $url = 'https://metrics.brainstormforce.com/wp-json/bsf-metrics-server/v1/subscribe';
252
253 if ( defined( 'SUREDONATION_METRICS_ENDPOINT' ) && is_string( SUREDONATION_METRICS_ENDPOINT ) ) {
254 $url = SUREDONATION_METRICS_ENDPOINT;
255 }
256
257 /**
258 * Filters the endpoint.
259 *
260 * @since 1.1.2
261 *
262 * @param string $url Endpoint URL.
263 * @param array<string,mixed> $payload Lead payload being sent.
264 */
265 $filtered = apply_filters( 'suredonation_metrics_subscribe_url', $url, $payload );
266 $url = is_string( $filtered ) ? $filtered : $url;
267
268 if ( '' === $url ) {
269 return false;
270 }
271
272 $first_name = isset( $payload['first_name'] ) && is_string( $payload['first_name'] ) ? $payload['first_name'] : '';
273 $last_name = isset( $payload['last_name'] ) && is_string( $payload['last_name'] ) ? $payload['last_name'] : '';
274 $domain = wp_parse_url( home_url(), PHP_URL_HOST );
275 $domain = is_string( $domain ) ? $domain : '';
276
277 $body = wp_json_encode(
278 [
279 // Lowercase keys satisfy the current BSF Metrics REST args.
280 'email' => $email,
281 'first_name' => $first_name,
282 'last_name' => $last_name,
283 'domain' => $domain,
284 'source' => 'suredonation',
285 // Legacy uppercase keys kept for backward compatibility.
286 'EMAIL' => $email,
287 'FIRSTNAME' => $first_name,
288 'LASTNAME' => $last_name,
289 'DOMAIN' => $domain,
290 ]
291 );
292
293 if ( false === $body ) {
294 return false;
295 }
296
297 // `source` identifies the originating plugin on the shared CRM server.
298 // wp_safe_remote_post with WP's default 5s timeout keeps a slow or
299 // hung endpoint from stalling onboarding completion.
300 $response = wp_safe_remote_post(
301 $url,
302 [
303 'headers' => [ 'Content-Type' => 'application/json' ],
304 'body' => $body,
305 ]
306 );
307
308 if ( is_wp_error( $response ) ) {
309 return false;
310 }
311
312 $code = (int) wp_remote_retrieve_response_code( $response );
313 return in_array( $code, [ 200, 201, 204 ], true );
314 }
315 }
316