# PushRow for Google Sheets (free and Pro)

Sync WordPress, WooCommerce, form, course and booking data with Google Sheets: exports, imports, real-time sync and schedules. Built by [WPAnkit](https://wpankit.com/) · [getpushrow.com](https://getpushrow.com/)

This one codebase builds both editions: **PushRow for Google Sheets** (free, WordPress.org slug `pushrow-for-google-sheets`) and **PushRow Pro** (premium, sold through Freemius, folder `pushrow-for-google-sheets-pro`). See [Free and Pro](#free-and-pro).

User documentation lives at [getpushrow.com/docs](https://getpushrow.com/docs/). `readme.txt` is the readme for both editions (WordPress.org and the Freemius plugin details screen); this file is for developers.

## Requirements

- PHP 7.4+
- WordPress 6.0+
- Composer, for the Google API client in `vendor/`

## Local setup

1. Clone the repository into `wp-content/plugins/pushrow-for-google-sheets-pro`.
2. Run `composer install` (dev dependencies include PHPUnit and Brain Monkey).
3. Activate the plugin and open **PushRow Pro** in the admin menu to connect a Google account.

The plugin uses `google/apiclient` but keeps only the Sheets and Drive services; `composer.json` runs Google's cleanup script on install.

## Free and Pro

Freemius generates the free plugin from the premium ZIP by deleting premium code. Everything premium therefore lives where Freemius can find it:

```
pushrow-for-google-sheets.php      Main file for both editions. Header lists @fs_premium_only.
pushrow-for-google-sheets-pro.php  Pro only: keeps sites that activated the old Pro main file running.
includes/                          The free plugin (core).
pro/                               PushRow Pro. Removed from the free build.
  bootstrap.php                    Defines PUSHROW_PRO_PREMIUM, registers the PushRowPro\Pro\ autoloader.
  class-module.php                 Plugs every Pro feature into the core hooks.
  class-import-engine.php          Sheet → WordPress runner, import targets.
  class-scheduler.php, class-realtime-sync.php, class-bulk-processor.php,
  class-webhook-receiver.php, class-apps-script-generator.php, class-sync-state.php
  admin/class-rest-api.php         Pro REST routes.
  providers/, targets/, integrations/ (Gravity Forms, WPForms)
  assets/js/admin-pro.js           Pro half of the admin app.
  assets/css/admin-pro.css         Styles for the Pro screens.
```

What each edition has:

| | Free | Pro |
|---|---|---|
| Posts and pages, users, Contact Form 7 | ✅ | ✅ |
| Manual runs, test runs, pause/resume, duplicate, activity log | ✅ | ✅ |
| Custom post types + ACF, WooCommerce, other form plugins, EDD, LearnDash, Amelia, database tables | | ✅ |
| Imports (Sheet → WordPress), schedules, real-time sync, bulk processing, Sheet push | | ✅ |

### The rules

1. **Core never refers to Pro.** Nothing in `includes/`, `assets/` or the main file (outside its `is__premium_only()` block) names a `PushRowPro\Pro\` class or a `pro/` path. Pro reaches the core only through the hooks below. `tests/Unit/ArchitectureTest.php` fails the build if this breaks.
2. **Pro code lives in `pro/` under the `PushRowPro\Pro\` namespace.** The main file lists `/pro/` under `@fs_premium_only` and loads `pro/bootstrap.php` inside `if ( pushrow_pro_fs()->is__premium_only() )`. Freemius removes both.
3. **The free build has no locked features.** Where Pro adds something, the free admin shows a plain "PushRow Pro" upgrade prompt; the feature's code isn't there at all. That keeps the free plugin within the WordPress.org guidelines.
4. **Stored data is shared.** Both editions use the same tables, options and keys (`pushrow_pro_*`), so upgrading from free to Pro keeps every job and setting. `Plugin::maybe_migrate_from_free_plugin()` brings over what the old standalone free plugin (0.0.2, `pushrow_*`) stored.
5. **`PushRowPro\Edition`** is the single place that knows the edition: `Edition::is_pro()` (true when `pro/bootstrap.php` ran), `Edition::features()` (feature flags for the admin app) and `Edition::upgrade_url()`.

### PHP hooks the core fires

| Hook | Type | Pro uses it to |
|---|---|---|
| `pushrow_pro_loaded` ( `Plugin $plugin` ) | action | boot its services from the core ones |
| `pushrow_pro_register_providers` ( `Sync_Engine` ) | action | register its data providers |
| `Sync_Engine::register_runner( $direction, $callable )` | method | run `sheet_to_wp` jobs (imports) |
| `pushrow_pro_post_types` | filter | offer every public post type, not only posts and pages |
| `pushrow_pro_features` | filter | switch its feature flags on |
| `pushrow_pro_job_saved` ( `Job`, `$context` ) / `pushrow_pro_job_deleted` ( `$id` ) | action | keep schedules in step |
| `pushrow_pro_rest_build_job` ( `Job`, `$params` ) | action | read its job fields (schedule, real-time, target, webhook secret) |
| `pushrow_pro_rest_job` ( `$data`, `Job` ) | filter | add the next scheduled run |
| `pushrow_pro_rest_provider_fields` | filter | return a database table's columns |
| `pushrow_pro_activated` | action | create its tables on activation and update |
| `pushrow_pro_enqueue_admin_assets` ( `$handle` ) | action | load `admin-pro.js` and `admin-pro.css` |

Pro fires `pushrow_pro_register_targets` ( `Import_Engine` ) for import targets.

### Admin app slots

`assets/js/admin-app.js` exposes named slots through [`wp.hooks`](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-hooks/) filters called `pushrow.<slot>`. Each slot passes the free default (usually an upgrade prompt) and uses what comes back; `pro/assets/js/admin-pro.js` fills them. `window.PushRowAdmin` gives extensions the app's state and helpers (`api`, `render`, `esc`, `pickCard`, `onClick`, `onChange`, `actions`, …).

| Slot | Free default | Pro |
|---|---|---|
| `pages` | `/automation` is an upgrade page | the Automation screen |
| `boot` | – | loads import targets and schedule intervals |
| `route` (action) | – | loads real-time events on `/automation` |
| `tabs.badge`, `dashboard.automationKpi`, `dashboard.importCard`, `dashboard.exportText` | "Pro" prompts | counts, KPI and the import card |
| `jobs.nextRunColumn`, `logs.types`, `settings.planCard` | hidden / export only / upgrade card | next run, import and real-time logs, license card |
| `wizard.directions`, `wizard.step2`, `wizard.providerGroups`, `wizard.hiddenProviders`, `wizard.step2Footer` | export only, "more data types" prompt | import direction and target step, Pro data type groups |
| `wizard.validate`, `wizard.beforeStep4`, `wizard.importMapping`, `wizard.mappingTools`, `wizard.uniqueOptions` | – | import checks, mapping and "Match by name" |
| `wizard.beforeStep5`, `wizard.step5Title`, `wizard.step5Panels`, `wizard.runTitle`, `wizard.scheduleField`, `wizard.reviewRows` | "Runs when you click Run now" | schedule, real-time and import settings |
| `wizard.payload`, `wizard.preload`, `wizard.steps` | core fields | Pro job fields and preloads |

### Adding a Pro feature

1. Put the PHP in `pro/` under `PushRowPro\Pro\…` and wire it in `Module::boot()` (or `Module::register()` if it must hook in before `plugins_loaded`).
2. If the core must call into it, add a hook to the core first, then use it from `pro/`. Never `class_exists()` a Pro class from the core.
3. For the admin app, add a `filter( 'my.slot', freeDefault )` call in `admin-app.js` and fill it from `admin-pro.js`. Put the free default's copy in the "Upgrade prompts" section.
4. Run `vendor/bin/phpunit`.

### Building and releasing

1. Bump the version in the plugin header, `PUSHROW_PRO_VERSION` and the `readme.txt` stable tag.
2. Push to `main`. `.github/workflows/release.yml` runs both test suites and builds `pushrow-for-google-sheets-pro-{version}.zip` (production dependencies only).
3. Upload that ZIP to Freemius. Freemius generates the free version from it, as long as the product has a **Free** plan.
4. Download the free version from Freemius and commit it to WordPress.org SVN (`trunk/` and a `tags/{version}/` copy). The listing's banners and icon are in `.wordpress-org/`.

`readme.txt` serves both editions: text between `[//]: # fs_free_only_begin` and `fs_free_only_end` is only in the free build, and text between the `fs_premium_only` markers only in Pro.

### Freemius settings

The SDK is set up with `slug` `pushrow-for-google-sheets` (the WordPress.org slug), `premium_slug` `pushrow-for-google-sheets-pro`, `premium_suffix` `Pro` and `is_org_compliant` true. Where it differs from the dashboard's SDK snippet, that's on purpose:

- The SDK is bundled in `includes/freemius/`, not installed with Composer in `vendor/freemius/`.
- `is_org_compliant` must be true because the free version is on WordPress.org: it stops the free plugin from installing premium code itself. Mark the product as hosted on WordPress.org in Freemius so the snippet agrees; it then also shows a `wp_org_gatekeeper` line to add to the `fs_dynamic_init()` array (Freemius removes it from the free build).
- `premium_suffix` is `Pro`: the SDK strips it from the plugin name to find the base name. Set the premium suffix to "Pro" in Freemius too.
- The `set_basename()` wrapper, `navigation` and the `menu` settings are local choices.

The product also needs a Free plan, or Freemius won't generate the free version.

## Data types

Providers export (WordPress → Sheet); targets import (Sheet → WordPress). Each appears only when its plugin is active. Everything below except posts, pages, users and Contact Form 7 export is Pro.

| Data type | Export | Import | Real-time | Scheduled |
|-----------|--------|--------|-----------|-----------|
| Posts & Pages (media too) | ✅ Free | ✅ | ✅ | ✅ |
| Custom Post Types + ACF | ✅ | ✅ core fields (via Posts) | ✅ | ✅ |
| Users | ✅ Free | ✅ | ✅ | ✅ |
| WooCommerce Orders | ✅ | ✅ status, notes, tracking (update only) | ✅ | ✅ |
| WooCommerce Products | ✅ | ✅ | ✅ | ✅ |
| WooCommerce Customers | ✅ | ❌ | ✅ | ✅ |
| WooCommerce Coupons | ✅ | ✅ | ✅ | ✅ |
| WooCommerce Refunds | ✅ | ❌ | ✅ | ✅ |
| Product Reviews | ✅ | ✅ | ✅ | ✅ |
| Product Categories & Tags | ✅ | ✅ | ✅ | ✅ |
| Contact Form 7 | ✅ Free | ❌ | ✅ on submission (Free) | ✅ |
| Gravity Forms | ✅ | ❌ | ✅ on submission | ✅ |
| WPForms | ✅ | ❌ | ✅ on submission | ✅ |
| Ninja Forms | ✅ | ❌ | ✅ on submission | ✅ |
| Fluent Forms | ✅ | ❌ | ✅ on submission | ✅ |
| Formidable Forms | ✅ | ❌ | ✅ on submission | ✅ |
| EDD Orders / Customers / Licenses (EDD 3) | ✅ | ❌ | ✅ | ✅ |
| LearnDash Enrollments / Quiz Results | ✅ | ❌ | ✅ | ✅ |
| Amelia Bookings | ✅ | ❌ | ✅ | ✅ |
| Custom Database Table (site prefix only) | ✅ | ✅ | ❌ | ✅ |

`pro/providers/class-elementor-provider.php` exists but is not registered in `Module::register_providers()`, so Elementor Forms is not available.

## REST API

All routes are under `/wp-json/pushrow/v1/` and need `manage_options`, except `GET /auth/callback` (Google's OAuth redirect, verified by a single-use state token) and `POST /webhook/sheet-push/{job_id}` (checked against the job's `X-PushRow-Secret`).

Core (both editions):

- **Auth:** `GET /auth/status`, `POST /auth/credentials`, `GET /auth/url`, `GET /auth/callback`, `POST /auth/disconnect`, `GET /auth/account`
- **Providers:** `GET /providers`, `GET /providers/{id}/fields`, `GET /providers/{id}/filters`
- **Spreadsheets:** `GET /spreadsheets`, `POST /spreadsheets/create`, `GET /spreadsheets/{id}/tabs`, `POST /spreadsheets/{id}/tabs`, `GET /sheets/{spreadsheet_id}/headers/{tab}`
- **Jobs:** `GET|POST /jobs`, `GET|PUT|DELETE /jobs/{id}`, `POST /jobs/{id}/run`, `POST /jobs/{id}/test-run`, `POST /jobs/{id}/duplicate`, `POST /jobs/{id}/pause`, `POST /jobs/{id}/resume`
- **Utilities:** `POST /export` (older job-profile endpoint), `POST /field-mapper/auto-map`, `POST /field-mapper/validate`, `GET /logs`, `POST /logs/clear`, `GET /debug-report`

Pro (`pro/admin/class-rest-api.php`, `pro/class-webhook-receiver.php`):

- **Real-time:** `GET /providers/{id}/triggers`
- **Targets:** `GET /targets`, `GET /targets/{id}/fields`, `GET /targets/{id}/unique-keys`, `GET /targets/{id}/settings`, `GET /targets/db_table/table-fields`, `GET /targets/db_table/table-unique-keys`
- **Schedules and CPTs:** `GET /scheduler/intervals`, `GET /cpt/list`
- **Sheet push:** `GET /jobs/{id}/apps-script`, `POST /jobs/{id}/generate-webhook-secret`, `POST /webhook/sheet-push/{job_id}`
- **Bulk:** `POST /bulk/export`, `POST /bulk/import`, `GET /bulk/status/{job_id}`, `POST /bulk/cancel/{job_id}`
- **Older import endpoint:** `POST /import`

In the free build, saving a job ignores the Pro fields, and a new Sheet → WordPress job is refused with "needs PushRow Pro".

## Extending

Register a provider (implements `PushRowPro\Providers\Data_Provider`), or with Pro an import target (implements `PushRowPro\Pro\Targets\Sync_Target`):

```php
add_action( 'pushrow_pro_register_providers', function ( $sync_engine ) {
	$sync_engine->register_provider( new My_Custom_Provider() );
} );

// PushRow Pro only: $import_engine is PushRowPro\Pro\Import_Engine.
add_action( 'pushrow_pro_register_targets', function ( $import_engine ) {
	$import_engine->register_target( new My_Custom_Target() );
} );
```

Most built-in providers and targets pass their field list through a `pushrow_pro_{name}_fields` filter (for example `pushrow_pro_posts_fields` or `pushrow_pro_target_wc_products_fields`). `pushrow_pro_edd_mask_license_key` masks EDD license keys in exports.

## Admin app

`assets/js/admin-app.js` is a dependency-free, hash-routed SPA (`#/dashboard`, `#/jobs`, `#/jobs/new`, `#/automation`, `#/logs`, `#/settings`, `#/wizard`) rendered from template strings, with all events handled by two delegated listeners. It only depends on `wp-hooks`. `assets/css/admin-app.css` is scoped to `.pr-app` and loads only on PushRow screens. Pro adds `pro/assets/js/admin-pro.js` and `pro/assets/css/admin-pro.css` through the slots above.

## Tests

```bash
composer install
vendor/bin/phpunit                    # both suites
vendor/bin/phpunit --testsuite Core   # free plugin (tests/Unit)
vendor/bin/phpunit --testsuite Pro    # Pro module (tests/Pro)
```

The suites use Brain Monkey with no WordPress bootstrap, so new WordPress function calls need stubs in the affected test. Core tests must not depend on `pro/`.

## License

GPL v2 or later.
