Skip to content
Open
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
7 changes: 4 additions & 3 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ jobs:
- { series: "8.3", patch: "8.3.36" }
- { series: "8.4", patch: "8.4.27" }
- { series: "8.5", patch: "8.5.12" }
wp-version: ["6.8", "latest"]
wp-version: ["6.8", "6.9", "7.0", "latest"]

steps:
- name: Checkout
Expand Down Expand Up @@ -62,7 +62,7 @@ jobs:
run: npm run env:start

- name: Run PHPUnit
run: npx wp-env run tests-cli -- bash -c "cd wp-content/plugins/$(basename "$GITHUB_WORKSPACE") && composer test"
run: npx wp-env run tests-cli -- bash -c "cd wp-content/plugins/$(basename "$GITHUB_WORKSPACE") && composer test:unit"

- name: Generate PHPUnit dashboard
if: always() && hashFiles('tests/phpunit/test-results/junit.xml') != ''
Expand All @@ -82,7 +82,8 @@ jobs:

e2e:
name: E2E tests
runs-on: ubuntu-latest
# Playwright does not support Ubuntu 26.04, which ubuntu-latest now uses.
runs-on: ubuntu-24.04
timeout-minutes: 60

steps:
Expand Down
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,34 @@ define( 'BEEHIIV_SSLVERIFY', false ); // optional; local private CA only

Post template for API payloads uses the plugin default `post_template_id`; omit `post_template_id` from the request when unset (`Newsletter\PostSettingsBuilder`).

## WordPress VIP runtime config

On WordPress VIP, the platform defines `VIP_BEEHIIV_CONFIG` before the plugin loads. It is a plain PHP array built from the config form in the VIP Dashboard. The plugin reads it in `Beehiiv\OAuth\Config`. The field is declared in `vip-manifest.yaml`.

| Key | Type | Required | Effect |
| -------------- | ---- | -------- | ---------------------------------------------------------------------- |
| `api_base_url` | url | No | Overrides the beehiiv API base URL. Empty or invalid values are ignored. |

The plugin resolves the API base URL in this order: `VIP_BEEHIIV_CONFIG['api_base_url']`, then the `BEEHIIV_API_BASE_URL` constant from `wp-config.php`, then the production default. The plugin never fatals on missing or malformed config. It falls back to the next source.

Valid example:

```php
define( 'VIP_BEEHIIV_CONFIG', [
'api_base_url' => 'https://api.example.test/v2',
] );
```

Incomplete example (setup in progress, the field saved blank). The plugin ignores the blank value and uses the default API URL:

```php
define( 'VIP_BEEHIIV_CONFIG', [
'api_base_url' => '',
] );
```

Test fixtures for the valid, minimal, incomplete and invalid states live in `tests/phpunit/fixtures/`.

## Linting

```bash
Expand Down
4 changes: 2 additions & 2 deletions beehiiv.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
/**
* Plugin Name: beehiiv
* Description: Official beehiiv WordPress plugin. Publish posts as newsletters and grow your audience with beehiiv.
* Version: 1.1.0
* Version: 1.2.0
* Author: beehiiv
* Text Domain: beehiiv
* Domain Path: /languages
Expand All @@ -18,7 +18,7 @@
defined( 'ABSPATH' ) || exit;

if ( ! defined( 'BEEHIIV_VERSION' ) ) {
define( 'BEEHIIV_VERSION', '1.1.0' );
define( 'BEEHIIV_VERSION', '1.2.0' );
}

if ( ! defined( 'BEEHIIV_PLUGIN_FILE' ) ) {
Expand Down
7 changes: 6 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,12 @@
}
},
"scripts": {
"test": "phpunit -c phpunit.xml.dist"
"test": [
"@test:unit",
"@test:e2e"
],
"test:unit": "phpunit -c phpunit.xml.dist",
"test:e2e": "npx playwright test"
},
"minimum-stability": "stable",
"require": {
Expand Down
2 changes: 1 addition & 1 deletion includes/API/Cache.php
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ public static function set_publications( array $items ): void {
/**
* Clear the cached publications list so the next read fetches it from beehiiv.
*
* @since x.x.x
* @since 1.1.0
*
* @return void
*/
Expand Down
103 changes: 85 additions & 18 deletions includes/API/Resources/Posts.php
Original file line number Diff line number Diff line change
Expand Up @@ -84,20 +84,22 @@ public static function create( string $publication_id, array $payload ): array {
/**
* Fetch a single post from a beehiiv publication.
*
* @param string $publication_id Publication ID.
* @param string $post_id beehiiv post ID.
* @return array{success: bool, post: array<string, mixed>, error: string}
* @since x.x.x
* @param string $publication_id Publication ID.
* @param string $post_id beehiiv post ID.
* @param string|null $expand Optional `expand` value, for example `free_email_content`.
* @return array{success: bool, post: array<string, mixed>, error: string, status_code: int|null}
* @since 1.1.0
*/
public static function get( string $publication_id, string $post_id ): array {
public static function get( string $publication_id, string $post_id, ?string $expand = null ): array {
$publication_id = trim( $publication_id );
$post_id = trim( $post_id );

if ( '' === $publication_id || '' === $post_id ) {
return [
'success' => false,
'post' => [],
'error' => 'Publication ID or post ID is empty.',
'success' => false,
'post' => [],
'error' => 'Publication ID or post ID is empty.',
'status_code' => null,
];
}

Expand All @@ -106,32 +108,35 @@ public static function get( string $publication_id, string $post_id ): array {
rawurlencode( $publication_id ),
rawurlencode( $post_id )
);
$response = Client::request( $path );
$response = Client::request( $path, null === $expand ? [] : [ 'expand' => $expand ] );

$wp_error = Client::get_last_wp_error();

if ( null !== $wp_error ) {
return [
'success' => false,
'post' => [],
'error' => $wp_error,
'success' => false,
'post' => [],
'error' => $wp_error,
'status_code' => null,
];
}

$status_code = Client::get_last_status_code();

if ( null !== $status_code && $status_code > 299 ) {
return [
'success' => false,
'post' => [],
'error' => self::format_error_message( $response, $status_code ),
'success' => false,
'post' => [],
'error' => self::format_error_message( $response, $status_code ),
'status_code' => $status_code,
];
}

return [
'success' => true,
'post' => isset( $response['data'] ) && is_array( $response['data'] ) ? $response['data'] : [],
'error' => '',
'success' => true,
'post' => isset( $response['data'] ) && is_array( $response['data'] ) ? $response['data'] : [],
'error' => '',
'status_code' => $status_code,
];
}

Expand Down Expand Up @@ -274,6 +279,51 @@ public static function get_creation_state( string $publication_id, string $post_
return $result;
}

/**
* Read a post together with its rendered free email content.
*
* @link https://developers.beehiiv.com/api-reference/posts/show
*
* @param string $publication_id Publication ID.
* @param string $post_id beehiiv post ID.
* @return array{
* success: bool,
* email_html: string,
* subject: string,
* preview_text: string,
* error: string,
* status_code: int|null
* }
* @since 1.2.0
*/
public static function get_email_content( string $publication_id, string $post_id ): array {
$fetched = self::get( $publication_id, $post_id, 'free_email_content' );
$result = [
'success' => false,
'email_html' => '',
'subject' => '',
'preview_text' => '',
'error' => $fetched['error'],
'status_code' => $fetched['status_code'],
];

if ( ! $fetched['success'] ) {
return $result;
}

$data = $fetched['post'];
$html = $data['content']['free']['email'] ?? '';

$result['success'] = true;
$result['email_html'] = is_string( $html ) ? $html : '';
$result['subject'] = self::first_non_empty( [ $data['email_settings']['email_subject_line'] ?? '' ] );
$result['preview_text'] = self::first_non_empty(
[ $data['email_settings']['email_preview_text'] ?? '', $data['subtitle'] ?? '' ]
);

return $result;
}

/**
* Send a test email of a beehiiv post to one or more addresses.
*
Expand Down Expand Up @@ -398,6 +448,23 @@ public static function update( string $publication_id, string $post_id, array $p
];
}

/**
* First value that is a non-empty string, or an empty string.
*
* @param array<int, mixed> $values Candidate values in priority order.
* @return string
* @since 1.2.0
*/
private static function first_non_empty( array $values ): string {
foreach ( $values as $value ) {
if ( is_string( $value ) && '' !== trim( $value ) ) {
return $value;
}
}

return '';
}

/**
* Build a log-friendly error string from an API error response.
*
Expand Down
13 changes: 9 additions & 4 deletions includes/Admin/Options.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,9 @@ final class Options {
* @var array<string, mixed>
*/
private const DEFAULTS = [
'publication_id' => '',
'post_template_id' => '',
'publication_id' => '',
'post_template_id' => '',
'require_template_choice' => false,
];

/**
Expand Down Expand Up @@ -81,15 +82,19 @@ public static function sanitize( $input ): array {
? sanitize_text_field( (string) $input['post_template_id'] )
: '';

// An unchecked checkbox is missing from the form, so absence means off.
$require_template_choice = ! empty( $input['require_template_choice'] );

// Saving settings clears the cached template list for the selected
// publication so the dropdown reflects any beehiiv-side changes.
if ( '' !== $publication_id ) {
Cache::delete_post_templates( $publication_id );
}

return [
'publication_id' => $publication_id,
'post_template_id' => $post_template_id,
'publication_id' => $publication_id,
'post_template_id' => $post_template_id,
'require_template_choice' => $require_template_choice,
];
}
}
40 changes: 40 additions & 0 deletions includes/Admin/Registrar.php
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,17 @@ private static function register_page_settings_section( string $page_slug ): voi
'label_for' => 'beehiiv_post_template_id',
]
);

add_settings_field(
'beehiiv_require_template_choice',
__( 'Template choice', 'beehiiv' ),
[ self::class, 'render_require_template_choice_field' ],
$page_slug,
self::PAGE_SETTINGS_SECTION_ID,
[
'label_for' => 'beehiiv_require_template_choice',
]
);
}

/**
Expand Down Expand Up @@ -244,4 +255,33 @@ class="button button-secondary beehiiv-refresh-button"
</p>
<?php
}

/**
* Require-template-choice checkbox field.
*
* @since 1.2.0
*/
public static function render_require_template_choice_field(): void {
$settings = Options::get();
$name = Config::OPTION_NAME . '[require_template_choice]';
?>
<input type="hidden" name="<?php echo esc_attr( $name ); ?>" value="0" />
<label for="beehiiv_require_template_choice">
<input
type="checkbox"
id="beehiiv_require_template_choice"
name="<?php echo esc_attr( $name ); ?>"
value="1"
<?php checked( ! empty( $settings['require_template_choice'] ) ); ?>
/>
<?php esc_html_e( 'Require editors to choose a post template', 'beehiiv' ); ?>
</label>
<p class="description">
<?php
// phpcs:ignore Generic.Files.LineLength.MaxExceeded,Generic.Files.LineLength.TooLong -- Single string for translators / i18n tools.
esc_html_e( 'When on, editors must pick a template for each new post. The default template is not applied automatically to those posts, and sending is blocked until a template is chosen.', 'beehiiv' );
?>
</p>
<?php
}
}
Loading
Loading