Skip to content

Commit bd89db5

Browse files
Jmosier69Jim Mosier
andauthored
chore: WordPress.org compliance audit, SEO readme, and PCP fixes (#4)
* chore: WordPress.org compliance audit, SEO readme, and PCP fixes Prepare the plugin for WordPress.org submission by addressing all 18 Plugin Directory Guidelines and fixing every error from the Plugin Check Plugin (PCP). Compliance & readme: - Rewrite readme.txt with SEO-optimized title, description, FAQ, screenshots section, and expanded External Services documentation listing every external domain individually with Terms/Privacy links - Update Plugin Name header in roi-insights.php to match readme title - Bump Tested up to from 6.7 to 6.9 (current WordPress release) - Remove all Ads Advisor references from readme.txt and README.md - Update call tracking rates to new pricing ($8/$6/$4 per number) - Add call recording credit rates to tier descriptions PCP fixes: - Replace raw GA4 <script src> tag with wp_enqueue_script() - Collapse multiline pixel echoes (TikTok, Pinterest, Nextdoor) to single lines so phpcs:ignore directives cover the escaped variables - Add phpcs:ignore annotation for $_POST JSON input with explanation Admin UX: - Fix notice dismissal: add AJAX handler to persist dismissed state in user_meta so is-dismissible notices stay hidden across page loads - Add code comment on Dashboard iframe explaining architectural choice (WordPress.org Guideline 8 — reviewer documentation) * fix: reset notice dismissal on license change, restore async GA4 loading - Clear roi_insights_license_notice_dismissed user meta in clear_cache() so license warnings reappear after key changes or expiration instead of staying permanently hidden. - Use WP 6.3+ script loading strategy array for GA4 gtag.js enqueue to preserve async loading behavior from the original raw script tag. * fix: clear stale license cache on key change Delete the _stale transient in clear_cache() so stale_or_invalid() cannot return data from a previous license key if the backend is temporarily unreachable after a key update. * fix: bump minimum WP version to 6.3 for script strategy compat The wp_enqueue_script() array args format (strategy => async) was introduced in WP 6.3. Bump Requires at least from 6.2 to 6.3 so the GA4 async enqueue works correctly. WP 6.2 has been EOL since November 2024. * docs: soften async performance claim in FAQ Acknowledge that inline pixel bootstrap snippets run on the main thread, while remote scripts are loaded async. * fix: add NonceVerification.Missing to phpcs ignore for get_body() PCP flags both .Missing and .Recommended variants for $_POST access. The nonce is verified in verify_request() which is called before get_body() in every handler. * feat: add plugin-zip script for WordPress.org submission npm run plugin-zip builds the frontend, assembles only the runtime files into a roi-insights/ directory, zips it, and cleans up. Excludes: .git, .gitignore, node_modules, src, tsconfig.json, package.json, package-lock.json, README.md (GitHub-only). * fix: remove standalone call log claim, clarify md-roi.js loading - Remove "Basic call log" as a standalone free feature bullet — the call log is part of the embedded dashboard service, not a plugin feature. Reword to make this clear. - Clarify that md-roi.js loads on all frontend pages as a local file (no external requests) before any license activation or pixel enablement. --------- Co-authored-by: Jim Mosier <jmosier69@Jims-Mac-mini.local>
1 parent e187330 commit bd89db5

File tree

10 files changed

+181
-85
lines changed

10 files changed

+181
-85
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ node_modules/
22
.playwright-cli/
33
*.log
44
.DS_Store
5+
roi-insights.zip

README.md

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -109,19 +109,18 @@ The free tier stays free forever. Paid tiers add AI-powered intelligence and dee
109109
| Header/footer scripts ||||
110110
| Call tracking (DNI) ||||
111111
| Basic call log ||||
112-
| Call tracking rate (per number/month) | $15.00 | $9.00 | $6.00 |
113-
| Call tracking rate (per minute) | $0.25 | $0.15 | $0.10 |
112+
| Call tracking rate (per number/month) | $8.00 | $6.00 | $4.00 |
113+
| Call tracking rate (per minute) | $0.20 | $0.15 | $0.10 |
114114
| AI weekly executive summary ||||
115-
| Call audio recording ||||
115+
| Call audio recording ||(2 credits/min) | (1 credit/min) |
116116
| AI call transcription & scoring ||||
117117
| Advanced UTM attribution ||||
118-
| Ads Advisor (24/7 spend watchdog) ||||
119118
| Custom reporting ||||
120119
| MCP AI agent access ||||
121120

122121
**Professional ($39.95/mo)** is built for businesses running paid ads. You get a weekly AI executive summary that tells you what's working, full call transcription and lead scoring, and lower call tracking rates. For a lot of businesses, the savings on call tracking alone covers the subscription — see the math below.
123122

124-
**Business ($199/mo)** is for high-volume advertisers and agencies. The Ads Advisor watches your Google Ads spend around the clock — alerting you to budget overruns, underperforming campaigns, and automated bidding changes that are quietly costing you money. You also get the lowest call tracking rates, custom reporting, and MCP AI agent access to query your marketing data programmatically.
123+
**Business ($199/mo)** is for high-volume advertisers and agencies. You get the lowest call tracking rates, custom reporting, and MCP AI agent access to query your marketing data programmatically.
125124

126125
**Founder's Club ($4,495 one-time)** — Lifetime access to Business tier features, available as a limited-time offer for early adopters. See [ROI Insights](https://roiknowledge.com/?utm_source=github&utm_medium=referral&utm_campaign=roi-insights-wp) for availability.
127126

@@ -133,19 +132,19 @@ It can — and often does. Because call tracking rates drop with higher tiers, t
133132

134133
| | Platform | Number | Minutes | Monthly Total |
135134
|--|:--------:|:------:|:-------:|:-------------:|
136-
| Free | $0 | $15.00 | $90.00 | **$105.00** |
137-
| Professional | $39.95 | $9.00 | $54.00 | **$102.95** |
135+
| Free | $0 | $8.00 | $72.00 | **$80.00** |
136+
| Professional | $39.95 | $6.00 | $54.00 | **$99.95** |
138137

139-
At 120 calls a month, Professional is cheaper than Free — and includes AI transcription, lead scoring, and weekly reports.
138+
At higher call volumes, the savings on per-minute rates start to offset the subscription — and you get AI transcription, lead scoring, and weekly reports.
140139

141140
**Agency running 15 numbers, 3,000 minutes/month:**
142141

143142
| | Platform | Numbers | Minutes | Monthly Total |
144143
|--|:--------:|:-------:|:-------:|:-------------:|
145-
| Professional | $39.95 | $135.00 | $450.00 | **$624.95** |
146-
| Business | $199.00 | $90.00 | $300.00 | **$589.00** |
144+
| Professional | $39.95 | $90.00 | $450.00 | **$579.95** |
145+
| Business | $199.00 | $60.00 | $300.00 | **$559.00** |
147146

148-
The jump from Professional to Business saves $35/month in telecom costs alone, plus adds the Ads Advisor and custom reporting.
147+
The jump from Professional to Business saves money on telecom costs at scale, plus adds custom reporting and MCP AI agent access.
149148

150149
---
151150

@@ -223,7 +222,7 @@ You can enable auto-recharge so your balance stays topped up automatically — n
223222

224223
Yes — genuinely free, not "free for 14 days." You do need a license key (even for the free tier), but you generate it right inside the WordPress admin: click **Sign in with Google** (or enter your email for a magic link), and a domain-bound key is created and activated automatically. The free tier gives you GTM injection, GA4 and Search Console connections, native toggles for six ad platforms, attribution tracking, built-in event adapters, header/footer script injection, and a basic call log.
225224

226-
Paid tiers add AI transcription, lead scoring, call recording, advanced attribution, and the Ads Advisor — but the core tracking infrastructure works perfectly without them.
225+
Paid tiers add AI transcription, lead scoring, call recording, and advanced attribution — but the core tracking infrastructure works perfectly without them.
227226

228227
### Do I need a Google Tag Manager account?
229228

includes/class-api.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ private function verify_request(): void {
7171

7272
/** Decode the JSON-encoded 'data' field sent by api.post(). */
7373
private function get_body(): array {
74-
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- already verified in verify_request().
74+
// phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- nonce verified in verify_request(); raw JSON cannot be sanitized with sanitize_text_field() without mangling it. Individual values are sanitized in each handler.
7575
$raw = isset( $_POST['data'] ) ? wp_unslash( $_POST['data'] ) : '{}';
7676
$decoded = json_decode( $raw, true );
7777
return is_array( $decoded ) ? $decoded : array();

includes/class-license.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,8 +262,12 @@ private function invalid( string $reason ): array {
262262

263263
/**
264264
* Clear the license cache (e.g., after saving a new key).
265+
* Also resets the notice dismissal flag so any new license issues
266+
* surface immediately instead of staying hidden.
265267
*/
266268
public function clear_cache(): void {
267269
delete_transient( self::CACHE_KEY );
270+
delete_transient( self::CACHE_KEY . '_stale' );
271+
delete_user_meta( get_current_user_id(), 'roi_insights_license_notice_dismissed' );
268272
}
269273
}

includes/class-roi-insights.php

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ private function __construct() {
4646
add_action( 'admin_menu', array( $this, 'register_admin_menu' ) );
4747
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_assets' ) );
4848
add_action( 'admin_notices', array( $this, 'maybe_show_license_notice' ) );
49+
add_action( 'wp_ajax_roi_insights_dismiss_notice', array( $this, 'handle_dismiss_notice' ) );
4950
}
5051

5152
public static function get_instance(): self {
@@ -217,10 +218,26 @@ public function maybe_show_license_notice(): void {
217218
}
218219

219220
$settings_url = admin_url( 'admin.php?page=roi-insights#settings' );
221+
$nonce = wp_create_nonce( 'roi_insights_dismiss_notice' );
220222
printf(
221-
'<div class="notice notice-warning is-dismissible"><p><strong>ROI Insights:</strong> %s <a href="%s">Go to Activation →</a></p></div>',
223+
'<div class="notice notice-warning is-dismissible" id="roi-insights-license-notice" data-nonce="%s"><p><strong>ROI Insights:</strong> %s <a href="%s">Go to Activation →</a></p></div>',
224+
esc_attr( $nonce ),
222225
esc_html( $reason ),
223226
esc_url( $settings_url )
224227
);
228+
// Persist dismissal via AJAX so the notice stays hidden across page loads.
229+
echo '<script>document.addEventListener("DOMContentLoaded",function(){var n=document.getElementById("roi-insights-license-notice");if(n){n.addEventListener("click",function(e){if(e.target.classList.contains("notice-dismiss")){var x=new XMLHttpRequest();x.open("POST","' . esc_url( admin_url( 'admin-ajax.php' ) ) . '");x.setRequestHeader("Content-Type","application/x-www-form-urlencoded");x.send("action=roi_insights_dismiss_notice&_wpnonce="+n.dataset.nonce)}})}});</script>' . "\n";
230+
}
231+
232+
/**
233+
* AJAX handler — persist notice dismissal in user meta.
234+
*/
235+
public function handle_dismiss_notice(): void {
236+
check_ajax_referer( 'roi_insights_dismiss_notice' );
237+
if ( ! current_user_can( 'manage_options' ) ) {
238+
wp_send_json_error( null, 403 );
239+
}
240+
update_user_meta( get_current_user_id(), 'roi_insights_license_notice_dismissed', '1' );
241+
wp_send_json_success();
225242
}
226243
}

includes/class-tracking.php

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ public function inject_head(): void {
8888
if ( $s['ga4Enabled'] && ! empty( $s['ga4Id'] ) ) {
8989
$ga4_id = esc_js( $s['ga4Id'] );
9090
echo "<!-- ROI Insights: Google Analytics 4 -->\n";
91-
echo "<script async src=\"https://www.googletagmanager.com/gtag/js?id=" . esc_attr( $s['ga4Id'] ) . "\"></script>\n"; // phpcs:ignore WordPress.Security.EscapeOutput
91+
wp_enqueue_script( 'roi-insights-ga4-gtag', 'https://www.googletagmanager.com/gtag/js?id=' . rawurlencode( $s['ga4Id'] ), array(), null, array( 'in_footer' => false, 'strategy' => 'async' ) ); // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion
9292
echo "<script>window.dataLayer=window.dataLayer||[];function gtag(){dataLayer.push(arguments);}gtag('js',new Date());gtag('config','" . $ga4_id . "');</script>\n"; // phpcs:ignore WordPress.Security.EscapeOutput
9393
}
9494

@@ -110,10 +110,7 @@ public function inject_head(): void {
110110
if ( $s['tiktokEnabled'] && ! empty( $s['tiktokId'] ) ) {
111111
$tt_id = esc_js( $s['tiktokId'] );
112112
echo "<!-- ROI Insights: TikTok Pixel -->\n";
113-
echo "
114-
<script>
115-
!function(w,d,t){w.TiktokAnalyticsObject=t;var ttq=w[t]=w[t]||[];ttq.methods=['page','track','identify','instances','debug','on','off','once','ready','alias','group','enableCookie','disableCookie'],ttq.setAndDefer=function(t,e){t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}};for(var i=0;i<ttq.methods.length;i++)ttq.setAndDefer(ttq,ttq.methods[i]);ttq.instance=function(t){for(var e=ttq._i[t]||[],n=0;n<ttq.methods.length;n++)ttq.setAndDefer(e,ttq.methods[n]);return e},ttq.load=function(e,n){var i='https://analytics.tiktok.com/i18n/pixel/events.js';ttq._i=ttq._i||{},ttq._i[e]=[],ttq._i[e]._u=i,ttq._t=ttq._t||{},ttq._t[e]=+new Date,ttq._o=ttq._o||{},ttq._o[e]=n||{};var o=document.createElement('script');o.type='text/javascript',o.async=!0,o.src=i+'?sdkid='+e+'&lib='+t;var a=document.getElementsByTagName('script')[0];a.parentNode.insertBefore(o,a)};ttq.load('" . $tt_id . "');ttq.page();}(window,document,'ttq');
116-
</script>\n"; // phpcs:ignore WordPress.Security.EscapeOutput
113+
echo "<script>!function(w,d,t){w.TiktokAnalyticsObject=t;var ttq=w[t]=w[t]||[];ttq.methods=['page','track','identify','instances','debug','on','off','once','ready','alias','group','enableCookie','disableCookie'],ttq.setAndDefer=function(t,e){t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}};for(var i=0;i<ttq.methods.length;i++)ttq.setAndDefer(ttq,ttq.methods[i]);ttq.instance=function(t){for(var e=ttq._i[t]||[],n=0;n<ttq.methods.length;n++)ttq.setAndDefer(e,ttq.methods[n]);return e},ttq.load=function(e,n){var i='https://analytics.tiktok.com/i18n/pixel/events.js';ttq._i=ttq._i||{},ttq._i[e]=[],ttq._i[e]._u=i,ttq._t=ttq._t||{},ttq._t[e]=+new Date,ttq._o=ttq._o||{},ttq._o[e]=n||{};var o=document.createElement('script');o.type='text/javascript',o.async=!0,o.src=i+'?sdkid='+e+'&lib='+t;var a=document.getElementsByTagName('script')[0];a.parentNode.insertBefore(o,a)};ttq.load('" . $tt_id . "');ttq.page();}(window,document,'ttq');</script>\n"; // phpcs:ignore WordPress.Security.EscapeOutput
117114
}
118115

119116
// Microsoft (Bing) UET Tag.
@@ -127,22 +124,14 @@ public function inject_head(): void {
127124
if ( $s['pinterestEnabled'] && ! empty( $s['pinterestId'] ) ) {
128125
$pt_id = esc_js( $s['pinterestId'] );
129126
echo "<!-- ROI Insights: Pinterest Tag -->\n";
130-
echo "
131-
<script>
132-
!function(e){if(!window.pintrk){window.pintrk=function(){window.pintrk.queue.push(Array.prototype.slice.call(arguments))};var n=window.pintrk;n.queue=[],n.version='3.0';var t=document.createElement('script');t.async=!0,t.src=e;var r=document.getElementsByTagName('script')[0];r.parentNode.insertBefore(t,r)}}('https://s.pinimg.com/ct/core.js');
133-
pintrk('load','" . $pt_id . "');pintrk('page');
134-
</script>\n"; // phpcs:ignore WordPress.Security.EscapeOutput
127+
echo "<script>!function(e){if(!window.pintrk){window.pintrk=function(){window.pintrk.queue.push(Array.prototype.slice.call(arguments))};var n=window.pintrk;n.queue=[],n.version='3.0';var t=document.createElement('script');t.async=!0,t.src=e;var r=document.getElementsByTagName('script')[0];r.parentNode.insertBefore(t,r)}}('https://s.pinimg.com/ct/core.js');pintrk('load','" . $pt_id . "');pintrk('page');</script>\n"; // phpcs:ignore WordPress.Security.EscapeOutput
135128
}
136129

137130
// Nextdoor Pixel.
138131
if ( $s['nextdoorEnabled'] && ! empty( $s['nextdoorId'] ) ) {
139132
$nd_id = esc_js( $s['nextdoorId'] );
140133
echo "<!-- ROI Insights: Nextdoor Pixel -->\n";
141-
echo "
142-
<script>
143-
(function(w,d,s,n,a){if(!w[n]){w[n]={},w[n].q=[];var t=d.createElement(s);t.async=!0,t.src='https://ads.nextdoor.com/public/pixel/ndp.js';var e=d.getElementsByTagName(s)[0];e.parentNode.insertBefore(t,e),w[n].track=function(p,o){w[n].q.push({p:p,o:o})}}w[n].track('PAGE_VIEW',{advertiser_id:'" . $nd_id . "'});
144-
})(window,document,'script','ndp');
145-
</script>\n"; // phpcs:ignore WordPress.Security.EscapeOutput
134+
echo "<script>(function(w,d,s,n,a){if(!w[n]){w[n]={},w[n].q=[];var t=d.createElement(s);t.async=!0,t.src='https://ads.nextdoor.com/public/pixel/ndp.js';var e=d.getElementsByTagName(s)[0];e.parentNode.insertBefore(t,e),w[n].track=function(p,o){w[n].q.push({p:p,o:o})}}w[n].track('PAGE_VIEW',{advertiser_id:'" . $nd_id . "'});})(window,document,'script','ndp');</script>\n"; // phpcs:ignore WordPress.Security.EscapeOutput
146135
}
147136

148137
// Custom head code.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
"private": true,
66
"scripts": {
77
"build": "wp-scripts build src/admin/index.tsx --output-path=build",
8-
"start": "wp-scripts start src/admin/index.tsx --output-path=build"
8+
"start": "wp-scripts start src/admin/index.tsx --output-path=build",
9+
"plugin-zip": "npm run build && rm -rf dist && mkdir -p dist/roi-insights && cp -r roi-insights.php readme.txt LICENSE includes build assets dist/roi-insights/ && cd dist && zip -r ../roi-insights.zip roi-insights && cd .. && rm -rf dist && echo '✓ roi-insights.zip ready for upload'"
910
},
1011
"devDependencies": {
1112
"@wordpress/scripts": "^30.0.0",

0 commit comments

Comments
 (0)