Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions .github/workflows/phpcs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
name: PHPCS

on:
push:
branches: [main,dev]
pull_request:

jobs:
phpcs:
name: WPCS lint
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.1'
coverage: none
tools: composer:v2, cs2pr

- name: Get Composer cache directory
id: composer-cache
run: echo "dir=$(composer config cache-files-dir)" >> "$GITHUB_OUTPUT"

- name: Cache Composer dependencies
uses: actions/cache@v4
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-phpcs-${{ hashFiles('**/composer.json') }}
restore-keys: |
${{ runner.os }}-composer-phpcs-

- name: Install dependencies
run: composer update --no-interaction --no-progress --prefer-dist

- name: Run PHPCS
run: vendor/bin/phpcs -q --report=checkstyle | cs2pr
47 changes: 47 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
name: Release

on:
push:
tags:
- 'v*'

permissions:
contents: write

jobs:
release:
name: Create GitHub Release
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Generate release notes
id: notes
run: |
previous_tag=$(git describe --tags --abbrev=0 "${GITHUB_REF_NAME}^" 2>/dev/null || true)
{
echo "notes<<EOF"
if [ -n "$previous_tag" ]; then
echo "## Changes since ${previous_tag}"
echo
git log --pretty=format:'- %s (%h)' "${previous_tag}..${GITHUB_REF_NAME}"
else
echo "## Changes"
echo
git log --pretty=format:'- %s (%h)' "${GITHUB_REF_NAME}"
fi
echo
echo EOF
} >> "$GITHUB_OUTPUT"

- name: Create release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ github.ref_name }}
name: ${{ github.ref_name }}
body: ${{ steps.notes.outputs.notes }}
draft: false
prerelease: ${{ contains(github.ref_name, '-') }}
43 changes: 43 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: Tests

on:
push:
branches: [main,dev]
pull_request:

jobs:
phpunit:
name: PHPUnit (PHP ${{ matrix.php }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
php: ['7.4', '8.0', '8.1', '8.2', '8.3']

steps:
- uses: actions/checkout@v4

- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
coverage: none
tools: composer:v2

- name: Get Composer cache directory
id: composer-cache
run: echo "dir=$(composer config cache-files-dir)" >> "$GITHUB_OUTPUT"

- name: Cache Composer dependencies
uses: actions/cache@v4
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ matrix.php }}-${{ hashFiles('**/composer.json') }}
restore-keys: |
${{ runner.os }}-composer-${{ matrix.php }}-

- name: Install dependencies
run: composer update --no-interaction --no-progress --prefer-dist

- name: Run PHPUnit
run: vendor/bin/phpunit --no-coverage
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
.DS_Store
node_modules
vendor
.phpunit.result.cache
.phpcs-cache
phpcs.xml
phpunit.xml


# local env files
Expand Down
154 changes: 135 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,58 +1,174 @@
# Freemius Plugin Licensing

This is a lite version of the main Freemius SDK, specifically developed for use in Duck Dev WordPress plugins. This
library focuses exclusively on managing plugin license activation, deactivation, and updates. It does not provide any
user interface, so your plugin will need to create its own UI and use this library to handle the logic.
A lite, UI-free Freemius SDK for Duck Dev WordPress plugins. The library handles license activation, deactivation,
update delivery, and addon listing by talking to the Freemius API directly. It deliberately ships no admin screens —
host plugins build their own UI and call into this library for the underlying logic.

## Requirements

* PHP version 7.4 or higher.
* PHP 7.4 or higher
* WordPress 5.0+
* Composer

## Installation

This library should be installed and included in your WordPress plugin using Composer.

```console
composer require duckdev/freemius-plugin-licensing
```

The library autoloads under the `DuckDev\Freemius\` namespace via PSR-4.

## Architecture

The library is organised as a small dependency-injection container wired up by the entry class
`DuckDev\Freemius\Freemius`. The folder layout mirrors the namespace:

```
src/
├── Freemius.php # Container + entry point
├── Api/
│ ├── Client.php # Unsigned HTTP client over wp_remote_request
│ ├── SignedClient.php # Adds FS / FSP signed auth headers
│ ├── RequestSigner.php # Pure header-signing logic
│ └── ApiFactory.php # Builds fresh clients per call
├── Contracts/
│ ├── ServiceInterface.php
│ ├── ApiClientInterface.php
│ └── CacheInterface.php
├── Data/
│ ├── Plugin.php # Immutable host plugin info
│ ├── Activation.php # Value object around the persisted activation
│ └── ApiKeys.php # Public / secret key pair
├── Storage/
│ ├── ActivationRepository.php # Reads / writes the activation option
│ └── TransientCache.php # Per-plugin transient cache + throttle
├── Services/
│ ├── AbstractService.php
│ ├── License.php # activate() / deactivate()
│ ├── Update.php # WP update hooks
│ └── Addon.php # Addon listing
├── Support/
│ └── SiteIdentity.php # Deterministic site UID
└── Exceptions/
└── FreemiusException.php
```

Each service receives its collaborators by constructor injection, so they can be unit-tested without WordPress in the
loop. Hook registration happens inside `boot()` (called once by the container), so simply instantiating the container
has no side effects.

## Usage

### Initialization

Initialize the Freemius SDK by calling the static `DuckDev\Freemius\Freemius::get_instance()` method with your plugin's
details.
Initialise the container by calling `Freemius::get_instance()` with your Freemius product ID and an arguments array:

```php
// Assuming Composer's autoload.php has been included.
$freemius = DuckDev\Freemius\Freemius::get_instance(
$freemius = \DuckDev\Freemius\Freemius::get_instance(
12345, // Your Freemius product ID.
array(
'slug' => 'loggedin', // Your plugin's unique Freemius slug.
'main_file' => LOGGEDIN_FILE, // The path to your plugin's main file.
'public_key' => 'pk_XXXXXXXXXXXXXXXXX', // Your plugin's public key.
'slug' => 'loggedin', // Your plugin's unique Freemius slug.
'main_file' => LOGGEDIN_FILE, // Absolute path to the plugin's main file.
'public_key' => 'pk_XXXXXXXXXXXXXXXXX', // Plugin public key.
'is_premium' => true, // Whether this build is the premium edition.
'has_addons' => false, // Whether the product has addons to list.
)
);
```

### License Activation
The supported arguments are:

| Key | Type | Description |
|---------------|----------|---------------------------------------------------------------------------------------------------|
| `slug` | `string` | Unique Freemius slug for the plugin. |
| `main_file` | `string` | Absolute path to the plugin's main file (used for `plugin_basename()` and `get_plugin_data()`). |
| `public_key` | `string` | Freemius public key (`pk_…`). Required for plugin-scoped endpoints (addons, info). |
| `is_premium` | `bool` | Whether this build is the premium edition. Update hooks only register when `true`. Default false. |
| `has_addons` | `bool` | Whether the product has addons to list. Default false. |

To activate a license, call the `activate()` method on the `license()` object with the user's license key.
The first call to `get_instance()` creates the container and registers WordPress hooks. Subsequent calls for the same
plugin ID return the existing instance (the second argument is ignored after the first call).

### License Activation

```php
$freemius->license()->activate( 'XXXX-XXXX-XXXX' );
$result = $freemius->license()->activate( 'XXXX-XXXX-XXXX' );

if ( is_wp_error( $result ) ) {
// $result->get_error_message() — show to the user.
}
```

`activate()` returns `true` / `false` from the option update on success, or a `WP_Error` when the key is empty, the
plugin is not the premium build, the API call fails, or the response does not include an install ID.

### License Deactivation

To deactivate a license, simply call the `deactivate()` method.
```php
$result = $freemius->license()->deactivate();
```

`deactivate()` refuses to proceed when the stored UID does not match the current site — that means the activation was
moved to another host, and we let the new host appear unlicensed rather than silently freeing the original seat.

### Reading the Current Activation

```php
$freemius->license()->deactivate();
$activation = $freemius->license()->get_activation();

if ( $activation->is_active() ) {
// $activation->license_key(), $activation->install_id(), …
}
```

`get_activation()` always returns an `Activation` value object — use `is_empty()` to detect the no-activation case.

### Updates

The library will automatically handle plugin updates as long as a valid license is active. No additional code is
required to check for and apply updates.
Update hooks are registered automatically during `boot()` for premium builds. There is no manual integration needed —
WordPress will check for, display, and apply updates through its standard pipeline.

To force a refresh from the host plugin's UI:

```php
$freemius->update()->get_update_data( true );
```

### Addons

```php
$addons = $freemius->addon()->get_addons(); // Cached for 24h.
$addons = $freemius->addon()->get_addons( true ); // Force refresh.
```

Each entry is enriched with a `link` field (Freemius checkout URL) and an `is_premium` boolean. Use the
`duckdev_freemius_format_addon_data` filter to add or rewrite fields per addon.

## Hooks

### Actions

| Hook | Arguments | When |
|---------------------------------------|--------------------------|---------------------------------------|
| `duckdev_freemius_license_activated` | `array $activation, bool $success` | After a successful activation. |
| `duckdev_freemius_license_deactivated`| `array $activation, bool $success` | After a successful deactivation. |

### Filters

| Hook | Arguments | Use |
|--------------------------------------------|-------------------------------------------------|------------------------------------------------------------------|
| `duckdev_freemius_api_request_args` | `array $args, string $method, string $url, array $data, array $headers` | Tweak the request arguments before they reach `wp_remote_request()`. |
| `duckdev_freemius_api_request_verify_ssl` | `bool $verify, Client $client` | Disable SSL verification (typically only in local dev). |
| `duckdev_freemius_format_addon_data` | `array $addon, Addon $service` | Rewrite or augment each addon entry before it is returned. |

## Security Notes

* The library does **not** verify nonces or capabilities. Host plugins MUST do that before forwarding form input to
`License::activate()` / `License::deactivate()`.
* The license key is stored in the `duckdev_freemius_activation_data` option (an autoload-safe option keyed by plugin
ID). It is blanked from storage on deactivation.

## License

GPL-2.0+
29 changes: 25 additions & 4 deletions composer.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "duckdev/freemius-plugin-licensing",
"version": "1.0.0",
"version": "2.0.0",
"type": "library",
"description": "Lite version of the Freemius SDK for managing plugin licensing and updates using Freemius APIs, specifically developed for use with Duck Dev plugins.",
"keywords": [
Expand All @@ -23,14 +23,35 @@
"require": {
"php": ">=7.4"
},
"require-dev": {
"phpunit/phpunit": "^9.6",
"brain/monkey": "^2.6",
"wp-coding-standards/wpcs": "^3.1",
"phpcompatibility/phpcompatibility-wp": "^2.1",
"dealerdirect/phpcodesniffer-composer-installer": "^1.0",
"squizlabs/php_codesniffer": "^3.10"
},
"config": {
"platform": {
"php": "7.4"
},
"allow-plugins": {
"dealerdirect/phpcodesniffer-composer-installer": true
}
},
"autoload": {
"classmap": [
"src/"
]
"psr-4": {
"DuckDev\\Freemius\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"DuckDev\\Freemius\\Tests\\": "tests/"
}
},
"scripts": {
"test": "phpunit",
"phpcs": "phpcs",
"phpcbf": "phpcbf"
}
}
Loading
Loading