PluginProbe
SureDonation – Donation Forms, Fundraising Campaigns & Donor Management / 1.1.0
SureDonation – Donation Forms, Fundraising Campaigns & Donor Management v1.1.0
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
← All changes | inc/api/onboarding-api.php +20 -316 1.6.0 → 1.1.0 View file →
@@ -4,13 +4,10 @@
4 4 *
5 5 * Routes (under `suredonation/v1`):
6 6 * - GET /onboarding/get-status — return { completed: 'yes'|'no' }
7 7 * - POST /onboarding/set-status — write completion + optional analytics
8 - * - POST /onboarding/create-campaign — create a published suredonation_cmpgn
8 + * - POST /onboarding/create-campaign — create a draft suredonation_cmpgn
9 9 * - POST /onboarding/user-details — persist lead capture (free-only step)
10 - * - POST /onboarding/set-tour-seen — mark the campaign guided tour seen (per-user meta)
11 - * - POST /onboarding/set-tour-progress — persist the tour's resume step (per-user meta)
12 - * - POST /onboarding/track-tour — fire the one-time "tour shown" analytics event
13 10 *
14 11 * @package SureDonation
15 12 */
16 13
@@ -42,46 +39,8 @@
42 39 */
43 40 private const GOAL_TYPES = [ 'raised_amount', 'donation_count' ];
44 41
45 42 /**
46 - * User-meta key for the "campaign guided tour seen" flag.
47 - *
48 - * Stored per-user (not a site option) so the "seen once" state follows the
49 - * user across devices, unlike the onboarding completion flag.
50 - *
51 - * @since 1.5.0
52 - * @var string
53 - */
54 - public const TOUR_SEEN_META = 'suredonation_campaign_tour_seen';
55 -
56 - /**
57 - * User-meta key for the campaign guided tour's resume point.
58 - *
59 - * Holds the step key the tour should resume at (empty string = no saved
60 - * progress). Persisted per-user so an interactive, multi-surface run
61 - * survives a page reload or a trip to the form builder / page editor.
62 - *
63 - * @since 1.5.0
64 - * @var string
65 - */
66 - public const TOUR_PROGRESS_META = 'suredonation_campaign_tour_progress';
67 -
68 - /**
69 - * User-meta key arming the first-run tour for a specific campaign.
70 - *
71 - * Holds the campaign id whose detail page should auto-start the guided tour
72 - * the first time it is opened. The onboarding wizard sets this because it
73 - * creates a campaign and then exits via a full page reload to the dashboard
74 - * / payments screen — so the in-memory `justCreated` navigation flag the SPA
75 - * flow relies on never reaches the campaign page. Cleared once the tour is
76 - * seen. Absent / 0 = nothing pending.
77 - *
78 - * @since 1.5.0
79 - * @var string
80 - */
81 - public const TOUR_PENDING_META = 'suredonation_campaign_tour_pending';
82 -
83 - /**
84 43 * Return endpoint definitions for Rest_Api to register.
85 44 *
86 45 * @return array<string,mixed>
87 46 * @since 1.0.0
@@ -87,102 +46,39 @@
87 46 * @since 1.0.0
88 47 */
89 48 public function get_endpoints() {
90 49 return [
91 - '/onboarding/get-status' => [
50 + '/onboarding/get-status' => [
92 51 'methods' => WP_REST_Server::READABLE,
93 52 'callback' => [ $this, 'get_status' ],
94 53 'permission_callback' => [ $this, 'check_permissions' ],
95 54 ],
96 - '/onboarding/set-status' => [
55 + '/onboarding/set-status' => [
97 56 'methods' => WP_REST_Server::EDITABLE,
98 57 'callback' => [ $this, 'set_status' ],
99 58 'permission_callback' => [ $this, 'check_permissions' ],
100 59 ],
101 - '/onboarding/create-campaign' => [
60 + '/onboarding/create-campaign' => [
102 61 'methods' => WP_REST_Server::EDITABLE,
103 62 'callback' => [ $this, 'create_campaign' ],
104 63 'permission_callback' => [ $this, 'check_permissions' ],
105 64 ],
106 - '/onboarding/user-details' => [
65 + '/onboarding/user-details' => [
107 66 'methods' => WP_REST_Server::EDITABLE,
108 67 'callback' => [ $this, 'save_user_details' ],
109 68 'permission_callback' => [ $this, 'check_permissions' ],
110 69 ],
111 - '/onboarding/set-tour-seen' => [
112 - 'methods' => WP_REST_Server::EDITABLE,
113 - 'callback' => [ $this, 'set_tour_seen' ],
114 - 'permission_callback' => [ $this, 'check_permissions' ],
115 - ],
116 - '/onboarding/set-tour-progress' => [
117 - 'methods' => WP_REST_Server::EDITABLE,
118 - 'callback' => [ $this, 'set_tour_progress' ],
119 - 'permission_callback' => [ $this, 'check_permissions' ],
120 - ],
121 - '/onboarding/track-tour' => [
122 - 'methods' => WP_REST_Server::EDITABLE,
123 - 'callback' => [ $this, 'track_tour' ],
124 - 'permission_callback' => [ $this, 'check_permissions' ],
125 - 'args' => [
126 - // Omitted => "tour shown", the endpoint's original meaning.
127 - 'event' => [
128 - 'type' => 'string',
129 - 'required' => false,
130 - 'default' => '',
131 - 'validate_callback' => static function ( $value ) {
132 - return in_array(
133 - $value,
134 - [ '', 'shown', 'completed', 'dismissed', 'opted_out', 'manual_started' ],
135 - true
136 - );
137 - },
138 - ],
139 - // Free-form step key, only meaningful for `dismissed`. It is
140 - // sent on to analytics, so keep it to a bounded slug.
141 - 'step' => [
142 - 'type' => 'string',
143 - 'required' => false,
144 - 'default' => '',
145 - 'sanitize_callback' => 'sanitize_key',
146 - ],
147 - ],
148 - ],
149 70 ];
150 71 }
151 72
152 73 /**
153 - * Permission gate. Write requests (POST/PUT/PATCH/DELETE) additionally
154 - * require a valid wp_rest nonce, matching Donors_API — onboarding forwards a
155 - * lead to the BSF CRM, so the write boundary is pinned explicitly.
74 + * Permission gate.
156 75 *
157 - * @param \WP_REST_Request<array<string,mixed>>|null $request Current request.
158 - * @return bool|\WP_Error
76 + * @return bool
159 77 * @since 1.0.0
160 78 */
161 - public function check_permissions( $request = null ) {
162 - if ( ! current_user_can( 'manage_options' ) ) {
163 - return false;
164 - }
165 -
166 - if ( $request instanceof \WP_REST_Request ) {
167 - $method = strtoupper( $request->get_method() );
168 - if ( in_array( $method, [ 'POST', 'PUT', 'PATCH', 'DELETE' ], true ) ) {
169 - $nonce = $request->get_header( 'X-WP-Nonce' );
170 - if ( empty( $nonce ) ) {
171 - $nonce_param = $request->get_param( '_wpnonce' );
172 - $nonce = is_string( $nonce_param ) ? $nonce_param : '';
173 - }
174 - if ( empty( $nonce ) || ! wp_verify_nonce( $nonce, 'wp_rest' ) ) {
175 - return new \WP_Error(
176 - 'rest_forbidden',
177 - __( 'Invalid or missing nonce.', 'suredonation' ),
178 - [ 'status' => 403 ]
179 - );
180 - }
181 - }
182 - }
183 -
184 - return true;
79 + public function check_permissions() {
80 + return current_user_can( 'manage_options' );
185 81 }
186 82
187 83 /**
188 84 * GET /onboarding/get-status.
@@ -214,9 +110,9 @@
214 110
215 111 /**
216 112 * POST /onboarding/create-campaign.
217 113 *
218 - * Creates a published campaign post + writes its meta. Returns the new
114 + * Creates a draft campaign post + writes its meta. Returns the new
219 115 * campaign id + edit URL so the JS can persist it in onboarding state.
220 116 *
221 117 * @param WP_REST_Request $request Request.
222 118 * @return WP_REST_Response|WP_Error
@@ -248,16 +144,12 @@
248 144 if ( ! in_array( $goal_type, self::GOAL_TYPES, true ) ) {
249 145 $goal_type = 'raised_amount';
250 146 }
251 147
252 - // Publish the campaign so it behaves like one created via the normal
253 - // flow: the save_post_suredonation_cmpgn hook auto-creates its default
254 - // donation form, and the campaign becomes selectable in the Donation
255 - // Form block (whose query is limited to published campaigns).
256 148 $result = wp_insert_post(
257 149 [
258 150 'post_type' => Campaign_Cpt::POST_TYPE,
259 - 'post_status' => 'publish',
151 + 'post_status' => 'draft',
260 152 'post_title' => $name,
261 153 'post_excerpt' => $description,
262 154 'post_author' => get_current_user_id(),
263 155 ],
@@ -289,14 +181,8 @@
289 181 'goal_amount' => $goal_amount,
290 182 ]
291 183 );
292 184
293 - // Arm the first-run tour for this campaign: the wizard leaves via a full
294 - // page reload, so the SPA's transient `justCreated` flag never reaches the
295 - // campaign page. The pending pointer makes the tour fire the first time the
296 - // user opens this campaign's detail page instead.
297 - update_user_meta( get_current_user_id(), self::TOUR_PENDING_META, $campaign_id );
298 -
299 185 return new WP_REST_Response(
300 186 [
301 187 'success' => true,
302 188 'campaign_id' => $campaign_id,
@@ -315,10 +201,8 @@
315 201 * @return WP_REST_Response
316 202 * @since 1.0.0
317 203 */
318 204 public function save_user_details( $request ) {
319 - $onboarding = Onboarding::get_instance();
320 -
321 205 $payload = [
322 206 'first_name' => sanitize_text_field( (string) $request->get_param( 'first_name' ) ),
323 207 'last_name' => sanitize_text_field( (string) $request->get_param( 'last_name' ) ),
324 208 'email' => sanitize_email( (string) $request->get_param( 'email' ) ),
@@ -324,22 +208,26 @@
324 208 'email' => sanitize_email( (string) $request->get_param( 'email' ) ),
325 209 'opted_in' => (bool) $request->get_param( 'opted_in' ),
326 210 ];
327 211
328 - $onboarding->set_user_details( $payload );
212 + Onboarding::get_instance()->set_user_details( $payload );
329 213
214 + // Persist the usage-tracking opt-in as its own option so other
215 + // plugin code (analytics, telemetry pings) can check it without
216 + // loading the consolidated onboarding details. Site option — the
217 + // BSF Analytics library reads it via get_site_option(), so the
218 + // write must use the same scope to stay in sync on multisite.
330 219 update_site_option(
331 220 'suredonation_usage_optin',
332 221 $payload['opted_in'] ? 'yes' : 'no'
333 222 );
334 223
335 - if ( ! $onboarding->is_lead_sent() && $this->forward_lead_to_crm( $payload ) ) {
336 - $onboarding->mark_lead_sent();
337 - }
338 -
339 224 /**
340 225 * Fires after onboarding lead-capture details are persisted.
341 226 *
227 + * Listeners (e.g. Pro analytics) can forward the payload to a
228 + * metrics endpoint when `opted_in` is true.
229 + *
342 230 * @since 1.0.0
343 231 *
344 232 * @param array<string,mixed> $payload Sanitised payload.
345 233 */
@@ -345,190 +233,6 @@
345 233 */
346 234 do_action( 'suredonation_onboarding_user_details_saved', $payload );
347 235
348 236 return new WP_REST_Response( [ 'success' => true ] );
349 - }
350 -
351 - /**
352 - * POST /onboarding/set-tour-seen.
353 - *
354 - * Marks the campaign guided tour as seen for the current user so it does
355 - * not reappear on future campaign creations. Written when the user either
356 - * completes the tour or opts out via "Don't show again".
357 - *
358 - * @return WP_REST_Response
359 - * @since 1.5.0
360 - */
361 - public function set_tour_seen() {
362 - $user_id = get_current_user_id();
363 - update_user_meta( $user_id, self::TOUR_SEEN_META, 'yes' );
364 - // The tour is finished with — drop any saved resume point and the
365 - // wizard's pending pointer so neither can re-trigger it.
366 - delete_user_meta( $user_id, self::TOUR_PROGRESS_META );
367 - delete_user_meta( $user_id, self::TOUR_PENDING_META );
368 -
369 - return new WP_REST_Response( [ 'success' => true ] );
370 - }
371 -
372 - /**
373 - * POST /onboarding/set-tour-progress.
374 - *
375 - * Persists the step the guided tour should resume at (per-user), so an
376 - * interactive run survives a reload or a trip to another screen. Stored as
377 - * `"<campaign_id>:<step>"` so a run only resumes on the campaign it started
378 - * on. An empty `step` (or missing campaign) clears the saved point.
379 - *
380 - * @param WP_REST_Request $request Request.
381 - * @return WP_REST_Response
382 - * @since 1.5.0
383 - */
384 - public function set_tour_progress( $request ) {
385 - $user_id = get_current_user_id();
386 - $step = sanitize_key( (string) $request->get_param( 'step' ) );
387 - $campaign_id = absint( $request->get_param( 'campaign_id' ) );
388 -
389 - if ( '' === $step || $campaign_id <= 0 ) {
390 - delete_user_meta( $user_id, self::TOUR_PROGRESS_META );
391 - } else {
392 - update_user_meta( $user_id, self::TOUR_PROGRESS_META, $campaign_id . ':' . $step );
393 - }
394 -
395 - return new WP_REST_Response( [ 'success' => true ] );
396 - }
397 -
398 - /**
399 - * POST /onboarding/track-tour.
400 - *
401 - * Signals a campaign guided-tour analytics moment. With no `event` param this
402 - * means "the tour was shown", which is what the endpoint did originally and
403 - * what an older script bundle still sends. An `event` param instead reports
404 - * how a run ended (completed / dismissed / opted_out) or that a replay was
405 - * started manually.
406 - *
407 - * The actions below fire on every call; the built-in analytics listener
408 - * decides what to record and how often (the BSF events tracker dedups by
409 - * event name), so repeat calls are cheap.
410 - *
411 - * @param \WP_REST_Request<array<string,mixed>>|null $request Request object. A
412 - * missing request is treated as the bare "shown" signal, so the
413 - * endpoint's original no-argument contract still holds.
414 - * @return WP_REST_Response
415 - * @since 1.5.0
416 - */
417 - public function track_tour( $request = null ) {
418 - // Both params are read up front so the outcome branch below does not have
419 - // to re-establish that the request exists.
420 - $event = '';
421 - $step = '';
422 -
423 - if ( $request instanceof WP_REST_Request ) {
424 - $event = Helper::get_string_value( $request->get_param( 'event' ) );
425 - $step = Helper::get_string_value( $request->get_param( 'step' ) );
426 - }
427 -
428 - if ( '' === $event || 'shown' === $event ) {
429 - /**
430 - * Fires whenever the campaign guided tour is shown (i.e. on every call
431 - * to this endpoint). The built-in listener dedups recording per site;
432 - * additional listeners run on each fire and must dedup themselves if
433 - * they need once-only behavior.
434 - *
435 - * @since 1.5.0
436 - */
437 - do_action( 'suredonation_campaign_tour_shown' );
438 -
439 - return new WP_REST_Response( [ 'success' => true ] );
440 - }
441 -
442 - /**
443 - * Fires when a campaign guided-tour run ends, or when a manual replay
444 - * starts.
445 - *
446 - * @param string $event The outcome: 'completed', 'dismissed',
447 - * 'opted_out' or 'manual_started'.
448 - * @param string $step Step key the run ended on; empty when not applicable.
449 - * @since 1.5.0
450 - */
451 - do_action( 'suredonation_campaign_tour_outcome', $event, $step );
452 -
453 - return new WP_REST_Response( [ 'success' => true ] );
454 - }
455 -
456 - /**
457 - * Generate lead.
458 - *
459 - * @param array<string,mixed> $payload Sanitised lead-capture payload.
460 - * @return bool True when the CRM accepted the lead, false otherwise.
461 - * @since 1.1.2
462 - */
463 - private function forward_lead_to_crm( array $payload ) {
464 - $email_raw = $payload['email'] ?? '';
465 - $email = is_string( $email_raw ) ? sanitize_email( $email_raw ) : '';
466 - if ( empty( $email ) || ! is_email( $email ) ) {
467 - return false;
468 - }
469 -
470 - $url = 'https://metrics.brainstormforce.com/wp-json/bsf-metrics-server/v1/subscribe';
471 -
472 - if ( defined( 'SUREDONATION_METRICS_ENDPOINT' ) && is_string( SUREDONATION_METRICS_ENDPOINT ) ) {
473 - $url = SUREDONATION_METRICS_ENDPOINT;
474 - }
475 -
476 - /**
477 - * Filters the endpoint.
478 - *
479 - * @since 1.1.2
480 - *
481 - * @param string $url Endpoint URL.
482 - * @param array<string,mixed> $payload Lead payload being sent.
483 - */
484 - $filtered = apply_filters( 'suredonation_metrics_subscribe_url', $url, $payload );
485 - $url = is_string( $filtered ) ? $filtered : $url;
486 -
487 - if ( '' === $url ) {
488 - return false;
489 - }
490 -
491 - $first_name = isset( $payload['first_name'] ) && is_string( $payload['first_name'] ) ? $payload['first_name'] : '';
492 - $last_name = isset( $payload['last_name'] ) && is_string( $payload['last_name'] ) ? $payload['last_name'] : '';
493 - $domain = wp_parse_url( home_url(), PHP_URL_HOST );
494 - $domain = is_string( $domain ) ? $domain : '';
495 -
496 - $body = wp_json_encode(
497 - [
498 - // Lowercase keys satisfy the current BSF Metrics REST args.
499 - 'email' => $email,
500 - 'first_name' => $first_name,
501 - 'last_name' => $last_name,
502 - 'domain' => $domain,
503 - 'source' => 'suredonation',
504 - // Legacy uppercase keys kept for backward compatibility.
505 - 'EMAIL' => $email,
506 - 'FIRSTNAME' => $first_name,
507 - 'LASTNAME' => $last_name,
508 - 'DOMAIN' => $domain,
509 - ]
510 - );
511 -
512 - if ( false === $body ) {
513 - return false;
514 - }
515 -
516 - // `source` identifies the originating plugin on the shared CRM server.
517 - // wp_safe_remote_post with WP's default 5s timeout keeps a slow or
518 - // hung endpoint from stalling onboarding completion.
519 - $response = wp_safe_remote_post(
520 - $url,
521 - [
522 - 'headers' => [ 'Content-Type' => 'application/json' ],
523 - 'body' => $body,
524 - ]
525 - );
526 -
527 - if ( is_wp_error( $response ) ) {
528 - return false;
529 - }
530 -
531 - $code = (int) wp_remote_retrieve_response_code( $response );
532 - return in_array( $code, [ 200, 201, 204 ], true );
533 237 }
534 238 }