Skip to content
Draft
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
4 changes: 2 additions & 2 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 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
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
101 changes: 84 additions & 17 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}
* @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 x.x.x
*/
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 x.x.x
*/
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 x.x.x
*/
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
9 changes: 9 additions & 0 deletions includes/Editor/Meta.php
Original file line number Diff line number Diff line change
Expand Up @@ -132,4 +132,13 @@ final class Meta {
* @since x.x.x
*/
public const NEWSLETTER_SHOW_TITLE_IN_EMAIL = '_beehiiv_newsletter_show_title_in_email';

/**
* Whether the linked beehiiv post is a `draft` (kept for Preview) or `confirmed`.
*
* Empty means the link was made by a normal send and counts as confirmed.
*
* @since x.x.x
*/
public const BEEHIIV_POST_STATE = '_beehiiv_post_state';
}
6 changes: 6 additions & 0 deletions includes/Editor/PostSettings.php
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,11 @@ final class PostSettings {
'default' => '',
'readonly' => true,
],
Meta::BEEHIIV_POST_STATE => [
'type' => 'string',
'default' => '',
'readonly' => true,
],
Meta::NEWSLETTER_ERROR => [
'type' => 'string',
'default' => '',
Expand Down Expand Up @@ -359,6 +364,7 @@ public static function prepare_rest_post( $response, $post, $request ) {
Meta::BEEHIIV_POST_ID,
Meta::BEEHIIV_SCHEDULED_AT,
Meta::BEEHIIV_PUBLICATION_ID,
Meta::BEEHIIV_POST_STATE,
Meta::SEND_TO_NEWSLETTER,
Meta::NEWSLETTER_ERROR,
Meta::NEWSLETTER_ERROR_TYPE,
Expand Down
Loading
Loading