diff --git a/.hooks/pre-push b/.hooks/pre-push new file mode 100755 index 00000000..8972970e --- /dev/null +++ b/.hooks/pre-push @@ -0,0 +1,8 @@ +#!/bin/bash + +branch=$(git symbolic-ref HEAD | sed -e 's,.*/\(.*\),\1,') + +if [[ "$branch" = "master" ]]; then + echo "Error: pushing directly to the master branch is prohibited" + exit 1 +fi diff --git a/composer.json b/composer.json index a5650e16..9182412a 100644 --- a/composer.json +++ b/composer.json @@ -8,7 +8,6 @@ "phpcompatibility/phpcompatibility-wp": "^2.1", "composer/installers": "^2.0", "brainmaestro/composer-git-hooks": "^2.8", - "xwp/wp-dev-lib": "^1.3", "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0", "yoast/phpunit-polyfills": "^1.0", "phpunit/phpunit": "^7.0 || ^9.5", @@ -26,8 +25,9 @@ "extra": { "hooks": { "pre-commit": [ - "./vendor/xwp/wp-dev-lib/scripts/pre-commit && ./node_modules/.bin/lint-staged" + "./node_modules/.bin/lint-staged" ], + "pre-push": "./.hooks/pre-push", "commit-msg": [ "cat $1 | ./node_modules/.bin/commitlint" ] diff --git a/composer.lock b/composer.lock index ccb6a0ed..78cf396c 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "0a2c2fcfdde6c09cf804cbe54ed72069", + "content-hash": "8849e876e5bc1cf8f489df71d21ccd24", "packages": [ { "name": "drewm/mailchimp-api", @@ -806,16 +806,16 @@ }, { "name": "phpcompatibility/phpcompatibility-paragonie", - "version": "1.3.1", + "version": "1.3.2", "source": { "type": "git", "url": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie.git", - "reference": "ddabec839cc003651f2ce695c938686d1086cf43" + "reference": "bba5a9dfec7fcfbd679cfaf611d86b4d3759da26" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibilityParagonie/zipball/ddabec839cc003651f2ce695c938686d1086cf43", - "reference": "ddabec839cc003651f2ce695c938686d1086cf43", + "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibilityParagonie/zipball/bba5a9dfec7fcfbd679cfaf611d86b4d3759da26", + "reference": "bba5a9dfec7fcfbd679cfaf611d86b4d3759da26", "shasum": "" }, "require": { @@ -852,13 +852,14 @@ "paragonie", "phpcs", "polyfill", - "standards" + "standards", + "static analysis" ], "support": { "issues": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie/issues", "source": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie" }, - "time": "2021-02-15T10:24:51+00:00" + "time": "2022-10-25T01:46:02+00:00" }, { "name": "phpcompatibility/phpcompatibility-wp", @@ -1235,16 +1236,16 @@ }, { "name": "phpunit/phpunit", - "version": "9.6.6", + "version": "9.6.7", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "b65d59a059d3004a040c16a82e07bbdf6cfdd115" + "reference": "c993f0d3b0489ffc42ee2fe0bd645af1538a63b2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/b65d59a059d3004a040c16a82e07bbdf6cfdd115", - "reference": "b65d59a059d3004a040c16a82e07bbdf6cfdd115", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/c993f0d3b0489ffc42ee2fe0bd645af1538a63b2", + "reference": "c993f0d3b0489ffc42ee2fe0bd645af1538a63b2", "shasum": "" }, "require": { @@ -1318,7 +1319,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.6" + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.7" }, "funding": [ { @@ -1334,7 +1335,7 @@ "type": "tidelift" } ], - "time": "2023-03-27T11:43:46+00:00" + "time": "2023-04-14T08:58:40+00:00" }, { "name": "psr/container", @@ -2350,16 +2351,16 @@ }, { "name": "sirbrillig/phpcs-variable-analysis", - "version": "v2.11.8", + "version": "v2.11.16", "source": { "type": "git", "url": "https://github.com/sirbrillig/phpcs-variable-analysis.git", - "reference": "aaf902277f2889fddbdb37046ae02b9965e2cf0f" + "reference": "dc5582dc5a93a235557af73e523c389aac9a8e88" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sirbrillig/phpcs-variable-analysis/zipball/aaf902277f2889fddbdb37046ae02b9965e2cf0f", - "reference": "aaf902277f2889fddbdb37046ae02b9965e2cf0f", + "url": "https://api.github.com/repos/sirbrillig/phpcs-variable-analysis/zipball/dc5582dc5a93a235557af73e523c389aac9a8e88", + "reference": "dc5582dc5a93a235557af73e523c389aac9a8e88", "shasum": "" }, "require": { @@ -2367,7 +2368,7 @@ "squizlabs/php_codesniffer": "^3.5.6" }, "require-dev": { - "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0", + "dealerdirect/phpcodesniffer-composer-installer": "^0.7 || ^1.0", "phpcsstandards/phpcsdevcs": "^1.1", "phpstan/phpstan": "^1.7", "phpunit/phpunit": "^4.8.36 || ^5.7.21 || ^6.5 || ^7.0 || ^8.0 || ^9.0", @@ -2404,20 +2405,20 @@ "source": "https://github.com/sirbrillig/phpcs-variable-analysis", "wiki": "https://github.com/sirbrillig/phpcs-variable-analysis/wiki" }, - "time": "2022-09-08T18:35:53+00:00" + "time": "2023-03-31T16:46:32+00:00" }, { "name": "squizlabs/php_codesniffer", - "version": "3.7.1", + "version": "3.7.2", "source": { "type": "git", "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", - "reference": "1359e176e9307e906dc3d890bcc9603ff6d90619" + "reference": "ed8e00df0a83aa96acf703f8c2979ff33341f879" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/1359e176e9307e906dc3d890bcc9603ff6d90619", - "reference": "1359e176e9307e906dc3d890bcc9603ff6d90619", + "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/ed8e00df0a83aa96acf703f8c2979ff33341f879", + "reference": "ed8e00df0a83aa96acf703f8c2979ff33341f879", "shasum": "" }, "require": { @@ -2453,27 +2454,28 @@ "homepage": "https://github.com/squizlabs/PHP_CodeSniffer", "keywords": [ "phpcs", - "standards" + "standards", + "static analysis" ], "support": { "issues": "https://github.com/squizlabs/PHP_CodeSniffer/issues", "source": "https://github.com/squizlabs/PHP_CodeSniffer", "wiki": "https://github.com/squizlabs/PHP_CodeSniffer/wiki" }, - "time": "2022-06-18T07:21:10+00:00" + "time": "2023-02-22T23:07:41+00:00" }, { "name": "symfony/console", - "version": "v5.4.12", + "version": "v5.4.22", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "c072aa8f724c3af64e2c7a96b796a4863d24dba1" + "reference": "3cd51fd2e6c461ca678f84d419461281bd87a0a8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/c072aa8f724c3af64e2c7a96b796a4863d24dba1", - "reference": "c072aa8f724c3af64e2c7a96b796a4863d24dba1", + "url": "https://api.github.com/repos/symfony/console/zipball/3cd51fd2e6c461ca678f84d419461281bd87a0a8", + "reference": "3cd51fd2e6c461ca678f84d419461281bd87a0a8", "shasum": "" }, "require": { @@ -2538,12 +2540,12 @@ "homepage": "https://symfony.com", "keywords": [ "cli", - "command line", + "command-line", "console", "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v5.4.12" + "source": "https://github.com/symfony/console/tree/v5.4.22" }, "funding": [ { @@ -2559,7 +2561,7 @@ "type": "tidelift" } ], - "time": "2022-08-17T13:18:05+00:00" + "time": "2023-03-25T09:27:28+00:00" }, { "name": "symfony/deprecation-contracts", @@ -2712,16 +2714,16 @@ }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.26.0", + "version": "v1.27.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "433d05519ce6990bf3530fba6957499d327395c2" + "reference": "511a08c03c1960e08a883f4cffcacd219b758354" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/433d05519ce6990bf3530fba6957499d327395c2", - "reference": "433d05519ce6990bf3530fba6957499d327395c2", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/511a08c03c1960e08a883f4cffcacd219b758354", + "reference": "511a08c03c1960e08a883f4cffcacd219b758354", "shasum": "" }, "require": { @@ -2733,7 +2735,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.26-dev" + "dev-main": "1.27-dev" }, "thanks": { "name": "symfony/polyfill", @@ -2773,7 +2775,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.26.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.27.0" }, "funding": [ { @@ -2789,20 +2791,20 @@ "type": "tidelift" } ], - "time": "2022-05-24T11:49:31+00:00" + "time": "2022-11-03T14:55:06+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.26.0", + "version": "v1.27.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "219aa369ceff116e673852dce47c3a41794c14bd" + "reference": "19bd1e4fcd5b91116f14d8533c57831ed00571b6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/219aa369ceff116e673852dce47c3a41794c14bd", - "reference": "219aa369ceff116e673852dce47c3a41794c14bd", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/19bd1e4fcd5b91116f14d8533c57831ed00571b6", + "reference": "19bd1e4fcd5b91116f14d8533c57831ed00571b6", "shasum": "" }, "require": { @@ -2814,7 +2816,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.26-dev" + "dev-main": "1.27-dev" }, "thanks": { "name": "symfony/polyfill", @@ -2857,7 +2859,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.26.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.27.0" }, "funding": [ { @@ -2873,20 +2875,20 @@ "type": "tidelift" } ], - "time": "2022-05-24T11:49:31+00:00" + "time": "2022-11-03T14:55:06+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.26.0", + "version": "v1.27.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e" + "reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e", - "reference": "9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/8ad114f6b39e2c98a8b0e3bd907732c207c2b534", + "reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534", "shasum": "" }, "require": { @@ -2901,7 +2903,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.26-dev" + "dev-main": "1.27-dev" }, "thanks": { "name": "symfony/polyfill", @@ -2940,7 +2942,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.26.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.27.0" }, "funding": [ { @@ -2956,20 +2958,20 @@ "type": "tidelift" } ], - "time": "2022-05-24T11:49:31+00:00" + "time": "2022-11-03T14:55:06+00:00" }, { "name": "symfony/polyfill-php73", - "version": "v1.26.0", + "version": "v1.27.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php73.git", - "reference": "e440d35fa0286f77fb45b79a03fedbeda9307e85" + "reference": "9e8ecb5f92152187c4799efd3c96b78ccab18ff9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/e440d35fa0286f77fb45b79a03fedbeda9307e85", - "reference": "e440d35fa0286f77fb45b79a03fedbeda9307e85", + "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/9e8ecb5f92152187c4799efd3c96b78ccab18ff9", + "reference": "9e8ecb5f92152187c4799efd3c96b78ccab18ff9", "shasum": "" }, "require": { @@ -2978,7 +2980,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.26-dev" + "dev-main": "1.27-dev" }, "thanks": { "name": "symfony/polyfill", @@ -3019,7 +3021,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php73/tree/v1.26.0" + "source": "https://github.com/symfony/polyfill-php73/tree/v1.27.0" }, "funding": [ { @@ -3035,20 +3037,20 @@ "type": "tidelift" } ], - "time": "2022-05-24T11:49:31+00:00" + "time": "2022-11-03T14:55:06+00:00" }, { "name": "symfony/polyfill-php80", - "version": "v1.26.0", + "version": "v1.27.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "cfa0ae98841b9e461207c13ab093d76b0fa7bace" + "reference": "7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/cfa0ae98841b9e461207c13ab093d76b0fa7bace", - "reference": "cfa0ae98841b9e461207c13ab093d76b0fa7bace", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936", + "reference": "7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936", "shasum": "" }, "require": { @@ -3057,7 +3059,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.26-dev" + "dev-main": "1.27-dev" }, "thanks": { "name": "symfony/polyfill", @@ -3102,7 +3104,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.26.0" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.27.0" }, "funding": [ { @@ -3118,7 +3120,7 @@ "type": "tidelift" } ], - "time": "2022-05-10T07:21:04+00:00" + "time": "2022-11-03T14:55:06+00:00" }, { "name": "symfony/service-contracts", @@ -3205,16 +3207,16 @@ }, { "name": "symfony/string", - "version": "v5.4.12", + "version": "v5.4.22", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "2fc515e512d721bf31ea76bd02fe23ada4640058" + "reference": "8036a4c76c0dd29e60b6a7cafcacc50cf088ea62" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/2fc515e512d721bf31ea76bd02fe23ada4640058", - "reference": "2fc515e512d721bf31ea76bd02fe23ada4640058", + "url": "https://api.github.com/repos/symfony/string/zipball/8036a4c76c0dd29e60b6a7cafcacc50cf088ea62", + "reference": "8036a4c76c0dd29e60b6a7cafcacc50cf088ea62", "shasum": "" }, "require": { @@ -3271,7 +3273,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v5.4.12" + "source": "https://github.com/symfony/string/tree/v5.4.22" }, "funding": [ { @@ -3287,7 +3289,7 @@ "type": "tidelift" } ], - "time": "2022-08-12T17:03:11+00:00" + "time": "2023-03-14T06:11:53+00:00" }, { "name": "theseer/tokenizer", @@ -3390,59 +3392,18 @@ }, "time": "2020-05-13T23:57:56+00:00" }, - { - "name": "xwp/wp-dev-lib", - "version": "1.6.5", - "source": { - "type": "git", - "url": "https://github.com/xwp/wp-dev-lib.git", - "reference": "a62ccca94f9995f294e12d500c45856adff81e34" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/xwp/wp-dev-lib/zipball/a62ccca94f9995f294e12d500c45856adff81e34", - "reference": "a62ccca94f9995f294e12d500c45856adff81e34", - "shasum": "" - }, - "type": "library", - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "XWP", - "email": "technology@xwp.co", - "homepage": "https://xwp.co" - } - ], - "description": "Common code used during development of WordPress plugins and themes", - "homepage": "https://github.com/xwp/wp-dev-lib", - "keywords": [ - "development", - "plugins", - "themes", - "tools", - "wordpress" - ], - "support": { - "issues": "https://github.com/xwp/wp-dev-lib/issues", - "source": "https://github.com/xwp/wp-dev-lib" - }, - "time": "2020-10-16T04:03:40+00:00" - }, { "name": "yoast/phpunit-polyfills", - "version": "1.0.4", + "version": "1.0.5", "source": { "type": "git", "url": "https://github.com/Yoast/PHPUnit-Polyfills.git", - "reference": "3c621ff5429d2b1ff96dc5808ad6cde99d31ea4c" + "reference": "3b59adeef77fb1c03ff5381dbb9d68b0aaff3171" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Yoast/PHPUnit-Polyfills/zipball/3c621ff5429d2b1ff96dc5808ad6cde99d31ea4c", - "reference": "3c621ff5429d2b1ff96dc5808ad6cde99d31ea4c", + "url": "https://api.github.com/repos/Yoast/PHPUnit-Polyfills/zipball/3b59adeef77fb1c03ff5381dbb9d68b0aaff3171", + "reference": "3b59adeef77fb1c03ff5381dbb9d68b0aaff3171", "shasum": "" }, "require": { @@ -3450,13 +3411,12 @@ "phpunit/phpunit": "^4.8.36 || ^5.7.21 || ^6.0 || ^7.0 || ^8.0 || ^9.0" }, "require-dev": { - "yoast/yoastcs": "^2.2.1" + "yoast/yoastcs": "^2.3.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "1.x-dev", - "dev-develop": "1.x-dev" + "dev-main": "2.x-dev" } }, "autoload": { @@ -3490,7 +3450,7 @@ "issues": "https://github.com/Yoast/PHPUnit-Polyfills/issues", "source": "https://github.com/Yoast/PHPUnit-Polyfills" }, - "time": "2022-11-16T09:07:52+00:00" + "time": "2023-03-30T23:39:05+00:00" } ], "aliases": [], diff --git a/includes/class-newspack-popups-api.php b/includes/class-newspack-popups-api.php index 86adb04f..4af050ee 100644 --- a/includes/class-newspack-popups-api.php +++ b/includes/class-newspack-popups-api.php @@ -100,6 +100,26 @@ public function register_api_endpoints() { ], ] ); + + // API endpoints for RAS presets. + register_rest_route( + 'newspack-popups/v1', + '/reader-activation/campaign', + [ + 'methods' => \WP_REST_Server::READABLE, + 'callback' => [ $this, 'api_get_reader_activation_campaign_settings' ], + 'permission_callback' => [ $this, 'permission_callback' ], + ] + ); + register_rest_route( + 'newspack-popups/v1', + '/reader-activation/campaign', + [ + 'methods' => \WP_REST_Server::EDITABLE, + 'callback' => [ $this, 'api_update_reader_activation_campaign_settings' ], + 'permission_callback' => [ $this, 'permission_callback' ], + ] + ); } /** @@ -282,5 +302,41 @@ public function api_duplicate_popup( $request ) { $response = Newspack_Popups::duplicate_popup( $request['id'], $request['title'] ); return rest_ensure_response( $response ); } + + /** + * Get reader activation campaign settings. + * + * @param WP_REST_Request $request Request object. + * + * @return WP_REST_Response + */ + public function api_get_reader_activation_campaign_settings( $request ) { + $response = Newspack_Popups_Presets::get_ras_presets(); + + if ( \is_wp_error( $response ) ) { + return new \WP_REST_Response( [ 'message' => $response->get_error_message() ], 400 ); + } + + return rest_ensure_response( $response['prompts'] ); + } + + /** + * Update reader activation campaign settings. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. + */ + public function api_update_reader_activation_campaign_settings( $request ) { + $slug = $request['slug']; + $data = $request['data']; + + $response = Newspack_Popups_Presets::update_preset_prompt( $slug, $data ); + + if ( \is_wp_error( $response ) ) { + return new \WP_REST_Response( [ 'message' => $response->get_error_message() ], 400 ); + } + + return rest_ensure_response( $response['prompts'] ); + } } $newspack_popups_api = new Newspack_Popups_API(); diff --git a/includes/class-newspack-popups-exporter.php b/includes/class-newspack-popups-exporter.php index effef9c8..79a5e8ef 100644 --- a/includes/class-newspack-popups-exporter.php +++ b/includes/class-newspack-popups-exporter.php @@ -194,6 +194,14 @@ private function prepare_prompt_for_export( $prompt ) { unset( $prompt['options']['utm_suppression'] ); } + // We do not export custom taxonomies added to the popup. + $custom_taxonomies = Newspack_Popups_Model::get_custom_taxonomies(); + foreach ( $custom_taxonomies as $custom_tax ) { + if ( isset( $prompt[ $custom_tax ] ) ) { + unset( $prompt[ $custom_tax ] ); + } + } + return $prompt; } diff --git a/includes/class-newspack-popups-importer.php b/includes/class-newspack-popups-importer.php index 6123feaa..d642ec70 100644 --- a/includes/class-newspack-popups-importer.php +++ b/includes/class-newspack-popups-importer.php @@ -228,9 +228,17 @@ private function process_prompts() { $prompts = $this->pre_process_prompts_terms( $prompts ); foreach ( $prompts as $prompt ) { + $prompt_slug = isset( $prompt['slug'] ) ? $prompt['slug'] : null; + $prompt_content = $prompt['content']; + $user_input_fields = isset( $prompt['user_input_fields'] ) ? $prompt['user_input_fields'] : []; + + foreach ( $user_input_fields as $field ) { + $prompt_content = Newspack_Popups_Presets::process_user_inputs( $prompt_content, $field ); + } + $post_data = [ 'post_title' => $prompt['title'], - 'post_content' => $prompt['content'], + 'post_content' => $prompt_content, 'post_status' => $prompt['status'], 'post_type' => Newspack_Popups::NEWSPACK_POPUPS_CPT, ]; @@ -253,6 +261,11 @@ private function process_prompts() { if ( ! empty( $prompt['tags'] ) ) { Newspack_Popups_Model::set_popup_terms( $new_post, $prompt['tags'], 'post_tag' ); } + + // If there's a featured image. + if ( ! empty( $prompt['featured_image_id'] ) && false !== wp_get_attachment_url( (int) $prompt['featured_image_id'] ) ) { + set_post_thumbnail( $new_post, (int) $prompt['featured_image_id'] ); + } } } diff --git a/includes/class-newspack-popups-inserter.php b/includes/class-newspack-popups-inserter.php index 23bc7448..a4058200 100755 --- a/includes/class-newspack-popups-inserter.php +++ b/includes/class-newspack-popups-inserter.php @@ -48,6 +48,10 @@ public static function popups_for_post() { $preview_popup = Newspack_Popups_Model::retrieve_preview_popup( Newspack_Popups::previewed_popup_id() ); return [ $preview_popup ]; } + if ( Newspack_Popups::preset_popup_id() ) { + $preset_popup = Newspack_Popups_Presets::retrieve_preset_popup( Newspack_Popups::preset_popup_id() ); + return [ $preset_popup ]; + } // Popups disabled for this page. if ( self::assess_has_disabled_popups() ) { @@ -659,7 +663,9 @@ public static function enqueue_scripts() { * @return HTML */ public static function popup_shortcode( $atts = array() ) { - if ( isset( $atts['id'] ) ) { + if ( Newspack_Popups::preset_popup_id() ) { + $found_popup = Newspack_Popups_Presets::retrieve_preset_popup( Newspack_Popups::preset_popup_id() ); + } elseif ( isset( $atts['id'] ) ) { $include_unpublished = Newspack_Popups::is_preview_request(); $found_popup = Newspack_Popups_Model::retrieve_popup_by_id( $atts['id'], $include_unpublished ); } @@ -787,6 +793,11 @@ function( $popup_a, $popup_b ) { 'noPingback' => true, ]; + // If previewing a preset prompt, no need to include config for any prompts. + if ( Newspack_Popups::preset_popup_id() ) { + return; + } + // If previewing a specific prompt, no need to include config for all prompts. $previewed_popup_id = Newspack_Popups::previewed_popup_id(); if ( $previewed_popup_id ) { @@ -1057,7 +1068,17 @@ public static function should_display( $popup, $check_if_is_post = false ) { } $is_post_context_matching = $is_taxonomy_matching && in_array( $post_type, $supported_post_types ); - return $is_post_context_matching; + /** + * Filters the result of the should_display check for each prompt. + * + * If $check_result is false, it means it failed the previous checks. Changing this to true will make the prompt appear. + * Use it with caution as this might result in unexpected behavior. + * + * @param bool $check_result Whether the popup should be displayed. + * @param object $popup The popup to assess. + * @param bool $check_if_is_post Should the post type of post be taken into account. + */ + return apply_filters( 'newspack_popups_should_display_prompt', $is_post_context_matching, $popup, $check_if_is_post ); } /** diff --git a/includes/class-newspack-popups-model.php b/includes/class-newspack-popups-model.php index 0c82264f..9f77ee65 100644 --- a/includes/class-newspack-popups-model.php +++ b/includes/class-newspack-popups-model.php @@ -223,6 +223,44 @@ public static function retrieve_eligible_popups( $include_unpublished = false, $ return self::retrieve_popups_with_query( new WP_Query( $args ) ); } + /** + * Get an array of options from abbreviated query parameters. + * + * @return array Array of options. + */ + public static function get_preview_query_options() { + $options_filters = [ + 'background_color' => FILTER_SANITIZE_FULL_SPECIAL_CHARS, + 'display_title' => FILTER_VALIDATE_BOOLEAN, + 'hide_border' => FILTER_VALIDATE_BOOLEAN, + 'large_border' => FILTER_VALIDATE_BOOLEAN, + 'frequency' => FILTER_SANITIZE_FULL_SPECIAL_CHARS, + 'frequency_max' => FILTER_SANITIZE_FULL_SPECIAL_CHARS, + 'frequency_start' => FILTER_SANITIZE_FULL_SPECIAL_CHARS, + 'frequency_between' => FILTER_SANITIZE_FULL_SPECIAL_CHARS, + 'frequency_reset' => FILTER_SANITIZE_FULL_SPECIAL_CHARS, + 'overlay_color' => FILTER_SANITIZE_FULL_SPECIAL_CHARS, + 'overlay_opacity' => FILTER_SANITIZE_FULL_SPECIAL_CHARS, + 'overlay_size' => FILTER_SANITIZE_FULL_SPECIAL_CHARS, + 'no_overlay_background' => FILTER_VALIDATE_BOOLEAN, + 'placement' => FILTER_SANITIZE_FULL_SPECIAL_CHARS, + 'trigger_type' => FILTER_SANITIZE_FULL_SPECIAL_CHARS, + 'trigger_delay' => FILTER_SANITIZE_FULL_SPECIAL_CHARS, + 'trigger_scroll_progress' => FILTER_SANITIZE_FULL_SPECIAL_CHARS, + 'trigger_blocks_count' => FILTER_SANITIZE_FULL_SPECIAL_CHARS, + 'archive_insertion_posts_count' => FILTER_SANITIZE_FULL_SPECIAL_CHARS, + 'archive_insertion_is_repeating' => FILTER_VALIDATE_BOOLEAN, + 'utm_suppression' => FILTER_SANITIZE_FULL_SPECIAL_CHARS, + ]; + + $options = []; + foreach ( $options_filters as $option => $filter ) { + $options[ $option ] = filter_input( INPUT_GET, Newspack_Popups::PREVIEW_QUERY_KEYS[ $option ], $filter ); + } + + return $options; + } + /** * Retrieve popup preview CPT post. * @@ -236,33 +274,7 @@ public static function retrieve_preview_popup( $post_id ) { // Setting proper id for correct API calls. $post_object->ID = $post_id; - return self::create_popup_object( - $post_object, - false, - [ - 'background_color' => filter_input( INPUT_GET, 'n_bc', FILTER_SANITIZE_FULL_SPECIAL_CHARS ), - 'display_title' => filter_input( INPUT_GET, 'n_ti', FILTER_VALIDATE_BOOLEAN ), - 'hide_border' => filter_input( INPUT_GET, 'n_hb', FILTER_VALIDATE_BOOLEAN ), - 'large_border' => filter_input( INPUT_GET, 'n_lb', FILTER_VALIDATE_BOOLEAN ), - 'frequency' => filter_input( INPUT_GET, 'n_fr', FILTER_SANITIZE_FULL_SPECIAL_CHARS ), - 'frequency_max' => filter_input( INPUT_GET, 'n_fm', FILTER_SANITIZE_FULL_SPECIAL_CHARS ), - 'frequency_start' => filter_input( INPUT_GET, 'n_fs', FILTER_SANITIZE_FULL_SPECIAL_CHARS ), - 'frequency_between' => filter_input( INPUT_GET, 'n_fb', FILTER_SANITIZE_FULL_SPECIAL_CHARS ), - 'frequency_reset' => filter_input( INPUT_GET, 'n_ft', FILTER_SANITIZE_FULL_SPECIAL_CHARS ), - 'overlay_color' => filter_input( INPUT_GET, 'n_oc', FILTER_SANITIZE_FULL_SPECIAL_CHARS ), - 'overlay_opacity' => filter_input( INPUT_GET, 'n_oo', FILTER_SANITIZE_FULL_SPECIAL_CHARS ), - 'overlay_size' => filter_input( INPUT_GET, 'n_os', FILTER_SANITIZE_FULL_SPECIAL_CHARS ), - 'no_overlay_background' => filter_input( INPUT_GET, 'n_bg', FILTER_VALIDATE_BOOLEAN ), - 'placement' => filter_input( INPUT_GET, 'n_pl', FILTER_SANITIZE_FULL_SPECIAL_CHARS ), - 'trigger_type' => filter_input( INPUT_GET, 'n_tt', FILTER_SANITIZE_FULL_SPECIAL_CHARS ), - 'trigger_delay' => filter_input( INPUT_GET, 'n_td', FILTER_SANITIZE_FULL_SPECIAL_CHARS ), - 'trigger_scroll_progress' => filter_input( INPUT_GET, 'n_ts', FILTER_SANITIZE_FULL_SPECIAL_CHARS ), - 'trigger_blocks_count' => filter_input( INPUT_GET, 'n_tb', FILTER_SANITIZE_FULL_SPECIAL_CHARS ), - 'archive_insertion_posts_count' => filter_input( INPUT_GET, 'n_ac', FILTER_SANITIZE_FULL_SPECIAL_CHARS ), - 'archive_insertion_is_repeating' => filter_input( INPUT_GET, 'n_ar', FILTER_VALIDATE_BOOLEAN ), - 'utm_suppression' => filter_input( INPUT_GET, 'n_ut', FILTER_SANITIZE_FULL_SPECIAL_CHARS ), - ] - ); + return self::create_popup_object( $post_object, false, self::get_preview_query_options() ); } /** @@ -613,6 +625,12 @@ public static function create_popup_object( $campaign_post, $include_taxonomies $popup['categories'] = get_the_category( $id ); $popup['tags'] = get_the_tags( $id ); $popup['campaign_groups'] = get_the_terms( $id, Newspack_Popups::NEWSPACK_POPUPS_TAXONOMY ); + + $all_taxonomies = self::get_custom_taxonomies(); + + foreach ( $all_taxonomies as $custom_taxonomy ) { + $popup[ $custom_taxonomy ] = get_the_terms( $id, $custom_taxonomy ); + } } $duplicate_of = get_post_meta( $id, 'duplicate_of', true ); @@ -654,6 +672,16 @@ public static function create_popup_object( $campaign_post, $include_taxonomies return $popup; } + /** + * Gets custom taxonomies that are assigned to the popup post type. + * + * @return array + */ + public static function get_custom_taxonomies() { + $all_taxonomies = get_object_taxonomies( Newspack_Popups::NEWSPACK_POPUPS_CPT ); + return array_diff( $all_taxonomies, [ Newspack_Popups::NEWSPACK_POPUPS_TAXONOMY, 'category', 'post_tag' ] ); + } + /** * Get the popup delay in milliseconds. * @@ -1055,7 +1083,7 @@ public static function get_access_attrs( $popup ) { if ( Newspack_Popups_Settings::is_non_interactive() ) { return ''; } - if ( Newspack_Popups::previewed_popup_id() ) { + if ( Newspack_Popups::previewed_popup_id() || Newspack_Popups::preset_popup_id() ) { return ''; } // The amp-access endpoint is queried only once (on page load), but after changing block settings, @@ -1196,6 +1224,9 @@ public static function generate_popup( $popup ) { if ( Newspack_Popups::previewed_popup_id() ) { $popup = self::retrieve_preview_popup( Newspack_Popups::previewed_popup_id() ); } + if ( Newspack_Popups::preset_popup_id() ) { + $popup = Newspack_Popups_Presets::retrieve_preset_popup( Newspack_Popups::preset_popup_id() ); + } if ( self::is_overlay( $popup ) && self::is_overlay( $popup ) && has_block( 'newspack-blocks/homepage-articles', $popup['content'] ) ) { add_filter( 'newspack_blocks_homepage_enable_duplication', '__return_true' ); @@ -1227,7 +1258,7 @@ public static function generate_popup( $popup ) { $no_overlay_background = $popup['options']['no_overlay_background']; $hidden_fields = self::get_hidden_fields( $popup ); $is_newsletter_prompt = self::has_newsletter_prompt( $popup ); - $has_featured_image = has_post_thumbnail( $popup['id'] ); + $has_featured_image = has_post_thumbnail( $popup['id'] ) || ! empty( $popup['options']['featured_image_id'] ); $classes = array( 'newspack-lightbox', 'newspack-popup', 'newspack-lightbox-placement-' . $popup['options']['placement'], 'newspack-lightbox-size-' . $overlay_size ); $classes[] = ( ! empty( $popup['title'] ) && $display_title ) ? 'newspack-lightbox-has-title' : null; $classes[] = $hide_border ? 'newspack-lightbox-no-border' : null; @@ -1263,7 +1294,7 @@ class=""
diff --git a/includes/class-newspack-popups-presets.php b/includes/class-newspack-popups-presets.php new file mode 100644 index 00000000..2302f2d8 --- /dev/null +++ b/includes/class-newspack-popups-presets.php @@ -0,0 +1,377 @@ +ID = \wp_rand( 10000000, 10001000 ); // Make the ID really high to avoid collision with real post IDs. + $post_object->post_title = $preset['title']; + $post_object->post_author = 1; + $post_object->post_date = current_time( 'mysql' ); + $post_object->post_date_gmt = current_time( 'mysql', 1 ); + $post_object->post_content = $preset['content']; + $post_object->post_status = 'publish'; + $post_object->comment_status = 'closed'; + $post_object->ping_status = 'closed'; + $post_object->post_name = $slug; + $post_object->post_type = Newspack_Popups::NEWSPACK_POPUPS_CPT; + $post_object->filter = 'raw'; // Don't try to fetch from the wp_posts table. + $post_object = new \WP_Post( $post_object ); + + $options = Newspack_Popups_Model::get_preview_query_options(); + + if ( ! empty( $preset['featured_image_id'] ) ) { + $options['featured_image_id'] = $preset['featured_image_id']; + } + + return Newspack_Popups_Model::create_popup_object( $post_object, false, $options ); + } + + /** + * Get subscribable lists from the Newspack Newsletters plugin. + * + * @return array|WP_Error Array of lists. + */ + private static function get_newsletter_lists() { + $provider_lists = method_exists( '\Newspack_Newsletters_Subscription', 'get_lists' ) ? \Newspack_Newsletters_Subscription::get_lists() : []; + + if ( \is_wp_error( $provider_lists ) ) { + return $provider_lists; + } + + $lists = array_reduce( + $provider_lists, + function( $acc, $list ) { + if ( ! empty( $list['active'] ) ) { + $acc[] = [ + 'id' => $list['id'] ?? 0, + 'label' => $list['title'] ?? '', + 'description' => $list['description'] ?? '', + 'checked' => false, + ]; + } + + return $acc; + }, + [] + ); + + return $lists; + } + + /** + * Retrieve default prompts + segments. + * + * @return array|WP_Error Array of prompt and segment default configs. + */ + public static function get_ras_presets() { + $file = dirname( NEWSPACK_POPUPS_PLUGIN_FILE ) . '/presets/ras-defaults.json'; + + if ( ! is_readable( $file ) ) { + return new \WP_Error( 'newspack_popups_file_read_error', __( 'File not found or not readable.', 'newspack-popups' ) ); + } + + $data = wp_json_file_decode( $file, [ 'associative' => true ] ); + + if ( empty( $data ) || empty( $data['prompts'] ) || empty( $data['segments'] ) ) { + return new \WP_Error( 'newspack_popups_file_read_error', __( 'File is empty or invalid JSON.', 'newspack-popups' ) ); + } + + $saved_inputs = \get_option( self::NEWSPACK_POPUPS_RAS_PROMPTS_OPTION, [] ); + $lists = self::get_newsletter_lists(); + + if ( \is_wp_error( $lists ) ) { + return $lists; + } + + // Populate prompt configs with saved inputs. + $data['prompts'] = array_map( + function( $prompt ) use ( $lists, $saved_inputs ) { + // Check for saved inputs. + if ( ! empty( $saved_inputs[ $prompt['slug'] ] ) ) { + $fields = array_map( + function ( $field ) use ( $saved_inputs, $prompt ) { + if ( ! empty( $saved_inputs[ $prompt['slug'] ][ $field['name'] ] ) ) { + $field['value'] = $saved_inputs[ $prompt['slug'] ][ $field['name'] ]; + } + return $field; + }, + $prompt['user_input_fields'] + ); + $prompt['user_input_fields'] = $fields; + + // Mark as ready if all required inputs are filled. + if ( ! empty( $saved_inputs[ $prompt['slug'] ]['ready'] ) ) { + $prompt['ready'] = true; + } + } + + // Populate placeholder content with saved inputs or default values. + foreach ( $prompt['user_input_fields'] as $field ) { + if ( 'array' === $field['type'] || 'string' === $field['type'] ) { + $prompt['content'] = self::process_user_inputs( $prompt['content'], $field ); + } + if ( 'int' === $field['type'] && 'featured_image_id' === $field['name'] && isset( $field['value'] ) ) { + $prompt['featured_image_id'] = $field['value']; + } + } + + // Append newsletter list IDs as a selectable field. + if ( false !== strpos( $prompt['slug'], '_registration_' ) || false !== strpos( $prompt['slug'], '_newsletter_' ) ) { + $fields = array_map( + function( $field ) use ( $lists ) { + if ( 'lists' === $field['name'] ) { + $field['options'] = $lists; + $field['default'] = array_map( + function( $list ) { + return $list['id']; + }, + $lists + ); + } + + return $field; + }, + $prompt['user_input_fields'] + ); + $prompt['user_input_fields'] = $fields; + } + + // Get full URLs for help info screenshots. + if ( isset( $prompt['help_info']['screenshot'] ) ) { + $file = dirname( NEWSPACK_POPUPS_PLUGIN_FILE ) . '/src/assets/' . $prompt['help_info']['screenshot']; + if ( file_exists( $file ) ) { + $prompt['help_info']['screenshot'] = \plugin_dir_url( NEWSPACK_POPUPS_PLUGIN_FILE ) . 'src/assets/' . $prompt['help_info']['screenshot']; + } else { + unset( $prompt['help_info']['screenshot'] ); // Avoid a 404 if the referenced file doesn't exist. + } + } + + return $prompt; + }, + $data['prompts'] + ); + + return $data; + } + + /** + * Replace placeholders in a prompt's content with user input or default values. + * + * @param string $prompt_content Prompt content. + * @param array $field Field config. + * $field['name'] string Field name. Required. + * $field['type'] string Field value type: array or string. Required. + * $field['default'] string Field default value. Required. + * $field['value'] string Field user input value. + * $field['max_length'] int Max length of string-type user input value. + * + * @return string Prompt content with placeholders replaced. + */ + public static function process_user_inputs( $prompt_content, $field ) { + if ( ! isset( $field['name'] ) || ! isset( $field['type'] ) || ! isset( $field['default'] ) ) { + return $prompt_content; + } + + $field_name = $field['name']; + $value = isset( $field['value'] ) ? $field['value'] : $field['default']; + + // If a string, crop to max_length if set. + if ( 'string' === $field['type'] && isset( $field['max_length'] ) ) { + $value = substr( trim( $value ), 0, $field['max_length'] ); + } + // If an array, stringify with field name. + if ( 'array' === $field['type'] ) { + $value = '"' . $field_name . '": ' . \wp_json_encode( $value ); + } + + $prompt_content = str_replace( '{{' . $field_name . '}}', $value, $prompt_content ); + + return $prompt_content; + } + + /** + * Update saved inputs for a prompt preset. + * + * @param string $slug Slug name of the preset. + * @param array $inputs Array of user inputs, keyed by field name. + * @return boolean True if updated, false if not. + */ + public static function update_preset_prompt( $slug, $inputs ) { + $defaults = self::get_ras_presets(); + $saved_inputs = \get_option( self::NEWSPACK_POPUPS_RAS_PROMPTS_OPTION, [] ); + $updated = false; + $ready = true; + $missing_required = []; + $default_slugs = array_map( + function( $default_prompt ) { + return $default_prompt['slug']; + }, + $defaults['prompts'] + ); + $default_fields = array_reduce( + $defaults['prompts'], + function( $acc, $prompt ) use ( $slug ) { + if ( $prompt['slug'] === $slug ) { + $acc = $prompt['user_input_fields']; + } + return $acc; + }, + [] + ); + + // Validate prompt slug. + if ( ! in_array( $slug, $default_slugs, true ) ) { + return new \WP_Error( 'newspack_popups_update_ras_prompt_error', __( 'Invalid prompt slug.', 'newspack-popups' ) ); + } + + foreach ( $inputs as $field_name => $input ) { + $field_info = array_values( + array_filter( + $default_fields, + function( $field ) use ( $field_name ) { + return $field['name'] === $field_name; + } + ) + ); + + // Validate prompt fields. + if ( empty( $field_info ) ) { + return new \WP_Error( 'newspack_popups_update_ras_prompt_error', __( 'Invalid field name.', 'newspack-popups' ) ); + } + $field_info = $field_info[0]; + + // Save input value. + if ( isset( $input ) ) { + $saved_inputs[ $slug ][ $field_name ] = $input; + } + + // Determine ready state. + if ( isset( $field_info['required'] ) && $field_info['required'] && empty( $input ) ) { + $ready = false; + $missing_required[] = $field_info['label']; + } + } + + if ( 0 < count( $missing_required ) ) { + return new \WP_Error( + 'newspack_popups_update_ras_prompt_required_missing', + sprintf( + // Translators: %s is a list of missing required fields. + __( 'Missing required fields: %s', 'newspack-popups' ), + implode( ', ', $missing_required ) + ) + ); + } + + if ( $ready ) { + $saved_inputs[ $slug ]['ready'] = true; + } else { + unset( $saved_inputs[ $slug ]['ready'] ); + } + + \update_option( self::NEWSPACK_POPUPS_RAS_PROMPTS_OPTION, $saved_inputs ); + + return self::get_ras_presets(); + } + + /** + * Publish RAS preset prompts and segments with user inputted content, and unpublish all other prompts and segments. + * + * @return boolean|WP_Error True if successful, WP_Error if not. + */ + public static function activate_ras_presets() { + // Deactivate all existing segments. + $existing_segments = array_map( + function( $segment ) { + $segment['configuration']['is_disabled'] = true; + return $segment; + }, + Newspack_Popups_Segmentation::get_segments() + ); + \update_option( Newspack_Popups_Segmentation::SEGMENTS_OPTION_NAME, $existing_segments ); + + // Deactivate all existing prompts. + $existing_prompts = Newspack_Popups_Model::retrieve_active_popups(); + foreach ( $existing_prompts as $prompt ) { + $updated = \wp_update_post( + [ + 'ID' => $prompt['id'], + 'post_status' => 'draft', + ] + ); + + if ( \is_wp_error( $updated ) ) { + return $updated; + } + } + + // Get RAS presets with user input. + $presets = self::get_ras_presets(); + if ( empty( $presets['prompts'] || empty( $presets['segments'] ) ) ) { + return new \WP_Error( 'newspack_popups_activate_ras_prompts_error', __( 'Error creating preset prompts and segments. Please try again.', 'newspack-popups' ) ); + } + + // Set each prompt to "publish" status. + $presets['prompts'] = array_map( + function( $prompt ) { + $prompt['status'] = 'publish'; + return $prompt; + }, + $presets['prompts'] + ); + + $importer = new Newspack_Popups_Importer( $presets ); + + // Run the importer. + $result = $importer->import(); + + if ( ! empty( $result['errors'] ) && ! empty( array_filter( $result['errors'] ) ) ) { + if ( class_exists( '\Newspack\Logger' ) ) { + \Newspack\Logger::error( $result['errors'] ); + } else { + error_log( \wp_json_encode( $result['errors'], JSON_PRETTY_PRINT ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log + } + return new \WP_Error( 'newspack_popups_activate_ras_prompts_error', __( 'Error creating preset prompts and segments. Please try again.', 'newspack-popups' ) ); + } + + return true; + } +} diff --git a/includes/class-newspack-popups.php b/includes/class-newspack-popups.php index 28ff4636..bba23779 100644 --- a/includes/class-newspack-popups.php +++ b/includes/class-newspack-popups.php @@ -16,6 +16,7 @@ final class Newspack_Popups { const NEWSPACK_POPUPS_TAXONOMY = 'newspack_popups_taxonomy'; const NEWSPACK_POPUPS_ACTIVE_CAMPAIGN_GROUP = 'newspack_popups_active_campaign_group'; const NEWSPACK_POPUP_PREVIEW_QUERY_PARAM = 'pid'; + const NEWSPACK_POPUP_PRESET_QUERY_PARAM = 'preset'; const NEWSPACK_POPUPS_TAXONOMY_STATUS = 'newspack_popups_taxonomy_status'; const LIGHTWEIGHT_API_CONFIG_FILE_PATH_LEGACY = WP_CONTENT_DIR . '/../newspack-popups-config.php'; @@ -87,6 +88,7 @@ public function __construct() { add_filter( 'show_admin_bar', [ __CLASS__, 'show_admin_bar' ], 10, 2 ); // phpcs:ignore WordPressVIPMinimum.UserExperience.AdminBarRemoval.RemovalDetected include_once dirname( __FILE__ ) . '/class-newspack-popups-model.php'; + include_once dirname( __FILE__ ) . '/class-newspack-popups-presets.php'; include_once dirname( __FILE__ ) . '/class-newspack-popups-inserter.php'; include_once dirname( __FILE__ ) . '/class-newspack-popups-api.php'; include_once dirname( __FILE__ ) . '/class-newspack-popups-settings.php'; @@ -764,7 +766,7 @@ public static function is_preview_request() { $is_customizer_preview = is_customize_preview(); // Used by the Newspack Plugin's Campaigns Wizard. $is_view_as_preview = false != Newspack_Popups_View_As::viewing_as_spec(); - return ! empty( self::previewed_popup_id() ) || $is_view_as_preview || $is_customizer_preview; + return ! empty( self::previewed_popup_id() ) || ! empty( self::preset_popup_id() ) || $is_view_as_preview || $is_customizer_preview; } /** @@ -780,6 +782,19 @@ public static function previewed_popup_id() { return null; } + /** + * Get preset popup slug from the URL. + * + * @return string|null Popup slug, if found in the URL + */ + public static function preset_popup_id() { + // Not using filter_input since it's not playing well with phpunit. + if ( isset( $_GET[ self::NEWSPACK_POPUP_PRESET_QUERY_PARAM ] ) && $_GET[ self::NEWSPACK_POPUP_PRESET_QUERY_PARAM ] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + return sanitize_text_field( $_GET[ self::NEWSPACK_POPUP_PRESET_QUERY_PARAM ] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended + } + return null; + } + /** * Set default fields when Pop-up is created. * diff --git a/includes/schemas/class-prompts.php b/includes/schemas/class-prompts.php index b5620c05..e7357e90 100644 --- a/includes/schemas/class-prompts.php +++ b/includes/schemas/class-prompts.php @@ -25,17 +25,32 @@ public static function get_schema() { 'type' => 'object', 'additionalProperties' => false, 'properties' => [ - 'title' => [ + 'slug' => [ + 'name' => 'slug', // An optional unique slug to describe the purpose of the prompt. + 'type' => 'string', + 'required' => false, + ], + 'title' => [ 'name' => 'title', 'type' => 'string', 'required' => true, ], - 'content' => [ + 'content' => [ 'name' => 'content', 'type' => 'string', 'required' => true, ], - 'campaign_groups' => [ + 'featured_image_id' => [ + 'name' => 'featured_image_id', + 'type' => 'integer', // Attachment ID to be used as the featured image. + 'required' => false, + ], + 'ready' => [ + 'name' => 'ready', + 'type' => 'boolean', + 'required' => false, + ], + 'campaign_groups' => [ 'name' => 'campaign_groups', 'type' => 'array', 'required' => false, @@ -53,7 +68,7 @@ public static function get_schema() { ], ], ], - 'status' => [ + 'status' => [ 'name' => 'status', 'type' => 'string', 'required' => true, @@ -62,7 +77,7 @@ public static function get_schema() { 'draft', ], ], - 'categories' => [ + 'categories' => [ 'name' => 'categories', 'type' => 'array', 'required' => false, @@ -80,7 +95,7 @@ public static function get_schema() { ], ], ], - 'tags' => [ + 'tags' => [ 'name' => 'tags', 'type' => 'array', 'required' => false, @@ -98,7 +113,7 @@ public static function get_schema() { ], ], ], - 'options' => [ + 'options' => [ 'type' => 'object', 'required' => true, 'additionalProperties' => false, @@ -355,6 +370,91 @@ function( $item ) { ], ], ], + // Required user inputs when auto-generating a prompt. These will be shown as fields in the UI. + 'user_input_fields' => [ + 'name' => 'user_input_fields', + 'type' => 'array', + 'required' => false, + 'default' => [], + 'items' => [ + 'type' => 'object', + 'additionalProperties' => false, + 'properties' => [ + // Slug for the input. Must be unique per prompt. + 'name' => [ + 'type' => 'string', + ], + // Data type for the input. + 'type' => [ + 'type' => 'string', + ], + // Label for the input. + 'label' => [ + 'type' => 'string', + ], + // Help text describing the input. + 'description' => [ + 'type' => 'string', + ], + // Whether the input is required to generate the prompt or if it can be generated with an empty value. If true, the default value will be used when the input is empty. + 'required' => [ + 'type' => 'boolean', + ], + // Default value of the input. This will be populated by default in the UI. + 'default' => [ + 'type' => [ 'array', 'integer', 'string' ], + ], + // User-inputted value of the input, if available. + 'value' => [ + 'type' => [ 'array', 'integer', 'string' ], + ], + // If a string, maximum length for the input value. This will be used to validate the input in the UI. + 'max_length' => [ + 'type' => 'integer', + 'required' => false, + ], + // If an array, selectable options. + 'options' => [ + 'type' => 'array', + 'required' => false, + ], + ], + ], + ], + 'help_info' => [ + 'type' => 'object', + 'required' => false, + 'additionalProperties' => false, + 'properties' => [ + 'screenshot' => [ + 'name' => 'screenshot', + 'type' => 'string', + 'required' => false, + 'default' => '', // Filename of a screenshot to display. Will look for it in the plugin's src/assets folder. + ], + 'description' => [ + 'name' => 'description', + 'type' => 'string', + 'required' => false, + 'default' => '', + ], + 'recommendations' => [ + 'name' => 'recommendations', + 'type' => 'array', + 'required' => false, + 'items' => [ + 'type' => 'string', + ], + 'default' => [], + ], + 'url' => [ + 'name' => 'url', + 'type' => 'string', + 'required' => false, + 'default' => '', + ], + ], + ], ], ]; } diff --git a/package-lock.json b/package-lock.json index 3a96ca93..9744b957 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "dependencies": { "classnames": "^2.3.2", "intersection-observer": "^0.12.2", - "js-cookie": "^3.0.1", + "js-cookie": "^3.0.5", "lodash": "^4.17.21", "newspack-components": "^2.1.0", "qs": "^6.11.1", @@ -19,7 +19,7 @@ "devDependencies": { "@rushstack/eslint-patch": "^1.2.0", "eslint": "^7.32.0", - "lint-staged": "^13.2.0", + "lint-staged": "^13.2.1", "newspack-scripts": "^5.1.0", "postcss-scss": "^4.0.6", "prettier": "npm:wp-prettier@^2.2.1-beta-1", @@ -15985,11 +15985,11 @@ "dev": true }, "node_modules/js-cookie": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.1.tgz", - "integrity": "sha512-+0rgsUXZu4ncpPxRL+lNEptWMOWl9etvPHc/koSRp6MPwpRYAhmk0dUG00J4bxVV3r9uUzfo24wW0knS07SKSw==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", + "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==", "engines": { - "node": ">=12" + "node": ">=14" } }, "node_modules/js-tokens": { @@ -16327,9 +16327,9 @@ "integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=" }, "node_modules/lint-staged": { - "version": "13.2.0", - "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-13.2.0.tgz", - "integrity": "sha512-GbyK5iWinax5Dfw5obm2g2ccUiZXNGtAS4mCbJ0Lv4rq6iEtfBSjOYdcbOtAIFtM114t0vdpViDDetjVTSd8Vw==", + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-13.2.1.tgz", + "integrity": "sha512-8gfzinVXoPfga5Dz/ZOn8I2GOhf81Wvs+KwbEXQn/oWZAvCVS2PivrXfVbFJc93zD16uC0neS47RXHIjXKYZQw==", "dev": true, "dependencies": { "chalk": "5.2.0", @@ -43768,9 +43768,9 @@ "dev": true }, "js-cookie": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.1.tgz", - "integrity": "sha512-+0rgsUXZu4ncpPxRL+lNEptWMOWl9etvPHc/koSRp6MPwpRYAhmk0dUG00J4bxVV3r9uUzfo24wW0knS07SKSw==" + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", + "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==" }, "js-tokens": { "version": "4.0.0", @@ -44032,9 +44032,9 @@ "integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=" }, "lint-staged": { - "version": "13.2.0", - "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-13.2.0.tgz", - "integrity": "sha512-GbyK5iWinax5Dfw5obm2g2ccUiZXNGtAS4mCbJ0Lv4rq6iEtfBSjOYdcbOtAIFtM114t0vdpViDDetjVTSd8Vw==", + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-13.2.1.tgz", + "integrity": "sha512-8gfzinVXoPfga5Dz/ZOn8I2GOhf81Wvs+KwbEXQn/oWZAvCVS2PivrXfVbFJc93zD16uC0neS47RXHIjXKYZQw==", "dev": true, "requires": { "chalk": "5.2.0", diff --git a/package.json b/package.json index 91010e59..8c2a778e 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "lint": "npm run lint:scss && npm run lint:js", "lint:js": "eslint --ext .js,.jsx src", "lint:js:staged": "eslint --ext .js,.jsx", - "lint:php:staged": "./vendor/bin/phpcs", + "lint:php:staged": "./vendor/bin/phpcs --filter=GitStaged", "lint:scss": "stylelint '**/*.scss' --customSyntax postcss-scss --config=./node_modules/newspack-scripts/config/stylelint.config.js", "lint:scss:staged": "stylelint --customSyntax postcss-scss --config=./node_modules/newspack-scripts/config/stylelint.config.js", "format:js": "prettier 'src/**/*.{js,jsx}' --write", @@ -23,12 +23,14 @@ "test": "newspack-scripts test" }, "lint-staged": { - "*.scss": "npm run lint:scss:staged" + "*.js": "npm run lint:js:staged", + "*.scss": "npm run lint:scss:staged", + "*.php": "npm run lint:php:staged" }, "dependencies": { "classnames": "^2.3.2", "intersection-observer": "^0.12.2", - "js-cookie": "^3.0.1", + "js-cookie": "^3.0.5", "lodash": "^4.17.21", "newspack-components": "^2.1.0", "qs": "^6.11.1", @@ -37,7 +39,7 @@ "devDependencies": { "@rushstack/eslint-patch": "^1.2.0", "eslint": "^7.32.0", - "lint-staged": "^13.2.0", + "lint-staged": "^13.2.1", "newspack-scripts": "^5.1.0", "postcss-scss": "^4.0.6", "prettier": "npm:wp-prettier@^2.2.1-beta-1", diff --git a/presets/ras-defaults.json b/presets/ras-defaults.json index 6141a50d..b9f2646b 100644 --- a/presets/ras-defaults.json +++ b/presets/ras-defaults.json @@ -2,32 +2,36 @@ "prompts": [ { "status": "draft", - "title": "Inline Donation (secondary)", - "content": "\n
\n\n\n\n

Support our work<\/h2>\n\n\n\n

As an independent publication, we rely on donations to fund our journalism<\/p>\n\n\n", + "slug": "ras_registration_overlay", + "title": "Registration Overlay", + "content": "\n

{{success_message}}

<\/div>\n", "options": { "background_color": "#FFFFFF", "display_title": false, "hide_border": false, "large_border": false, - "frequency": "always", + "frequency": "custom", "frequency_max": 0, "frequency_start": 0, - "frequency_between": 0, - "frequency_reset": "month", + "frequency_between": "3", + "frequency_reset": "day", "overlay_color": "#000000", "overlay_opacity": "30", "overlay_size": "medium", "no_overlay_background": false, - "placement": "inline", - "trigger_type": "scroll", - "trigger_delay": "3", - "trigger_scroll_progress": "100", - "trigger_blocks_count": 0, + "placement": "center", + "trigger_type": "time", + "trigger_delay": "2", + "trigger_scroll_progress": 0, + "trigger_blocks_count": "3", "archive_insertion_posts_count": 1, "archive_insertion_is_repeating": false, - "selected_segment_id": "", + "selected_segment_id": "639b337fdbf33", "post_types": [ - "post" + "post", + "page", + "product", + "newspack_nl_cpt" ], "archive_page_types": [ "category", @@ -48,12 +52,77 @@ "id": 30, "name": "Reader Activation Defaults" } - ] + ], + "user_input_fields": [ + { + "name": "heading", + "type": "string", + "label": "Heading", + "description": "A heading to describe the prompt.", + "required": true, + "default": "Join our community for free. Sign up now.", + "max_length": 40 + }, + { + "name": "body", + "type": "string", + "label": "Body", + "description": "The primary call to action for the prompt.", + "required": true, + "default": "Register today to stay up-to-date with the latest news.", + "max_length": 300 + }, + { + "name": "button_label", + "type": "string", + "label": "Button Label", + "description": "The label for the primary CTA button.", + "required": true, + "default": "Sign up", + "max_length": 20 + }, + { + "name": "success_message", + "type": "string", + "label": "Success Message", + "description": "The message shown to readers after completing the signup.", + "required": true, + "default": "Thank you for registering!", + "max_length": 300 + }, + { + "name": "featured_image_id", + "type": "int", + "label": "Header Image (optional)", + "description": "Banner image to show at the top of the prompt.", + "default": 0 + }, + { + "name": "lists", + "type": "array", + "label": "Select Newsletters", + "description": "Newsletter lists the reader can subscribe to.", + "required": true, + "default": [] + } + ], + "help_info": { + "screenshot": "reg-overlay.png", + "description": "The Registration Overlay is shown to readers who have not yet registered nor signed up for a newsletter. It’s triggered on the first pageview, and then again every four pageviews.", + "recommendations": [ + "Focus on the benefits of creating an account.", + "Highlight the value that the reader will get for free.", + "Use action-oriented verbs like “join,” “register,” and “sign up.”", + "Create a sense of urgency/Encourage readers to take immediate action by using words like “now” and “today.”" + ], + "url": "https://help.newspack.com/engagement/reader-activation-system" + } }, { "status": "draft", - "title": "Registration Overlay", - "content": "\n
<\/div>\n", + "slug": "ras_newsletter_overlay", + "title": "Newsletter Signup Overlay", + "content": "\n

{{heading}}<\/h2>\n\n\n\n

{{body}}<\/p>\n\n\n", "options": { "background_color": "#FFFFFF", "display_title": false, @@ -75,7 +144,7 @@ "trigger_blocks_count": "3", "archive_insertion_posts_count": 1, "archive_insertion_is_repeating": false, - "selected_segment_id": "639b337fdbf33", + "selected_segment_id": "639b333b41025", "post_types": [ "post", "page", @@ -101,36 +170,104 @@ "id": 30, "name": "Reader Activation Defaults" } - ] + ], + "user_input_fields": [ + { + "name": "heading", + "type": "string", + "label": "Heading", + "description": "A heading to describe the prompt.", + "required": true, + "default": "Stay in the know", + "max_length": 40 + }, + { + "name": "body", + "type": "string", + "label": "Body", + "description": "The primary call to action for the prompt.", + "required": true, + "default": "Sign up to our free newsletter to get the latest news delivered straight to your inbox.", + "max_length": 300 + }, + { + "name": "button_label", + "type": "string", + "label": "Button Label", + "description": "The label for the primary CTA button.", + "required": true, + "default": "Sign up", + "max_length": 20 + }, + { + "name": "success_message", + "type": "string", + "label": "Success Message", + "description": "The message shown to readers after completing the signup.", + "required": true, + "default": "Thank you for signing up!", + "max_length": 300 + }, + { + "name": "featured_image_id", + "type": "int", + "label": "Header Image (optional)", + "description": "Banner image to show at the top of the prompt.", + "default": 0 + }, + { + "name": "lists", + "type": "array", + "label": "Select Newsletters", + "description": "Newsletter lists the reader can subscribe to.", + "required": true, + "default": [] + } + ], + "help_info": { + "screenshot": "newsletter-overlay.png", + "description": "The Newsletter Signup Overlay is shown to readers who have registered but not yet signed up for a newsletter. It’s triggered on the first pageview, and then again every four pageviews.", + "recommendations": [ + "Highlight the value the reader will get from the newsletter(s).", + "Emphasize that signing up is free.", + "Use action-oriented verbs like “join,” “register,” and “sign up.”", + "Create a sense of urgency. Encourage readers to take immediate action by using words like “now” and “today.”" + ], + "url": "https://help.newspack.com/engagement/reader-activation-system" + } }, { "status": "draft", - "title": "Inline Newsletter Signup (secondary)", - "content": "\n

Sign up for our free newsletter to receive the latest news

\n\n\n", + "slug": "ras_donation_overlay", + "title": "Donation Overlay", + "content": "\n

{{heading}}<\/h2>\n\n\n\n

{{body}}<\/p>\n\n\n", "options": { "background_color": "#FFFFFF", "display_title": false, "hide_border": false, "large_border": false, - "frequency": "always", + "frequency": "custom", "frequency_max": 0, "frequency_start": 0, - "frequency_between": 0, - "frequency_reset": "month", + "frequency_between": "3", + "frequency_reset": "day", "overlay_color": "#000000", "overlay_opacity": "30", "overlay_size": "medium", "no_overlay_background": false, - "placement": "inline", - "trigger_type": "blocks_count", - "trigger_delay": "3", + "placement": "center", + "trigger_type": "time", + "trigger_delay": "2", "trigger_scroll_progress": 0, - "trigger_blocks_count": 2, + "trigger_blocks_count": "3", "archive_insertion_posts_count": 1, "archive_insertion_is_repeating": false, - "selected_segment_id": "639b333b41025,639b337fdbf33", + "selected_segment_id": "639b3315239f0", "post_types": [ - "post" + "post", + "page", + "product", + "newspack_nl_cpt" ], "archive_page_types": [ "category", @@ -151,39 +288,93 @@ "id": 30, "name": "Reader Activation Defaults" } - ] + ], + "user_input_fields": [ + { + "name": "heading", + "type": "string", + "label": "Heading", + "description": "A heading to describe the prompt.", + "required": true, + "default": "Support our work", + "max_length": 40 + }, + { + "name": "body", + "type": "string", + "label": "Body", + "description": "The primary call to action for the prompt.", + "required": true, + "default": "As an independent publication, we rely on contributions from readers like you to fund our journalism.", + "max_length": 300 + }, + { + "name": "thanks_message", + "type": "string", + "label": "Thanks Message", + "description": "A message to thank the reader for their contribution.", + "required": true, + "default": "Thanks for your contribution!", + "max_length": 300 + }, + { + "name": "button_label", + "type": "string", + "label": "Button Label", + "description": "The label for the primary CTA button.", + "required": true, + "default": "Contribute Now", + "max_length": 20 + }, + { + "name": "featured_image_id", + "type": "int", + "label": "Header Image (optional)", + "description": "Banner image to show at the top of the prompt.", + "default": 0 + } + ], + "help_info": { + "screenshot": "donation-overlay.png", + "description": "The Donation Overlay is shown to readers who have registered and are signed up for at least one newsletter, but haven’t contributed yet. It’s triggered on the first pageview, and then again every four pageviews.", + "recommendations": [ + "Highlight the importance of supporting your publication.", + "Focus on the value readers add by contributing financially.", + "Use action-oriented verbs like “donate,” “give,” and “contribute.”", + "Create a sense of urgency. Encourage readers to take immediate action by using words like “now” and “today.”" + ], + "url": "https://help.newspack.com/engagement/reader-activation-system" + } }, { "status": "draft", - "title": "Newsletter Sign-up Overlay", - "content": "\n

Stay in the know<\/h2>\n\n\n\n

Sign up for our free newsletters to receive the latest news directly in your inbox.<\/p>\n\n\n", + "slug": "ras_newsletter_inline", + "title": "Inline Newsletter Signup (secondary)", + "content": "\n

{{body}}

\n\n\n", "options": { "background_color": "#FFFFFF", "display_title": false, "hide_border": false, "large_border": false, - "frequency": "custom", + "frequency": "always", "frequency_max": 0, "frequency_start": 0, - "frequency_between": "3", - "frequency_reset": "day", + "frequency_between": 0, + "frequency_reset": "month", "overlay_color": "#000000", "overlay_opacity": "30", "overlay_size": "medium", "no_overlay_background": false, - "placement": "center", - "trigger_type": "time", - "trigger_delay": "2", + "placement": "inline", + "trigger_type": "blocks_count", + "trigger_delay": "3", "trigger_scroll_progress": 0, - "trigger_blocks_count": "3", + "trigger_blocks_count": 2, "archive_insertion_posts_count": 1, "archive_insertion_is_repeating": false, - "selected_segment_id": "639b333b41025", + "selected_segment_id": "639b333b41025,639b337fdbf33", "post_types": [ - "post", - "page", - "product", - "newspack_nl_cpt" + "post" ], "archive_page_types": [ "category", @@ -204,39 +395,85 @@ "id": 30, "name": "Reader Activation Defaults" } - ] + ], + "user_input_fields": [ + { + "name": "body", + "type": "string", + "label": "Body", + "description": "The primary call to action for the prompt.", + "required": true, + "default": "Sign up for our free newsletter.", + "max_length": 300 + }, + { + "name": "button_label", + "type": "string", + "label": "Button Label", + "description": "The label for the primary CTA button.", + "required": true, + "default": "Sign up", + "max_length": 20 + }, + { + "name": "success_message", + "type": "string", + "label": "Success Message", + "description": "The message shown to readers after completing the signup.", + "required": true, + "default": "Thank you for signing up!", + "max_length": 300 + }, + { + "name": "lists", + "type": "array", + "label": "Select Newsletters", + "description": "Newsletter lists the reader can subscribe to.", + "required": true, + "default": [] + } + ], + "help_info": { + "screenshot": "newsletter-inline.png", + "description": "The Inline Newsletter Signup is embedded within articles and shown to readers who have not yet signed up for a newsletter whether they are registered or not. It’s triggered on the first pageview, and then again every four pageviews and is inserted after the second paragraph block in every article.", + "recommendations": [ + "Highlight the value the reader will get from the newsletter(s).", + "Emphasize that signing up is free.", + "Use action-oriented verbs like “join,” “register,” and “sign up.”", + "Consider including the sending frequency to set expectations." + ], + "url": "https://help.newspack.com/engagement/reader-activation-system" + } }, { "status": "draft", - "title": "Donation Overlay", - "content": "\n

Support our work<\/h2>\n\n\n\n

As an independent publication, we rely on donations to fund our journalism<\/p>\n\n\n", + "slug": "ras_donation_inline", + "title": "Inline Donation (secondary)", + "content": "\n


\n\n\n\n

{{heading}}<\/h2>\n\n\n\n

{{body}}<\/p>\n\n\n", "options": { "background_color": "#FFFFFF", "display_title": false, - "hide_border": false, + "hide_border": true, "large_border": false, - "frequency": "custom", + "frequency": "always", "frequency_max": 0, "frequency_start": 0, - "frequency_between": "3", - "frequency_reset": "day", + "frequency_between": 0, + "frequency_reset": "month", "overlay_color": "#000000", "overlay_opacity": "30", "overlay_size": "medium", "no_overlay_background": false, - "placement": "center", - "trigger_type": "time", - "trigger_delay": "2", - "trigger_scroll_progress": 0, - "trigger_blocks_count": "3", + "placement": "inline", + "trigger_type": "scroll", + "trigger_delay": "3", + "trigger_scroll_progress": "100", + "trigger_blocks_count": 0, "archive_insertion_posts_count": 1, "archive_insertion_is_repeating": false, - "selected_segment_id": "639b3315239f0", + "selected_segment_id": "", "post_types": [ - "post", - "page", - "product", - "newspack_nl_cpt" + "post" ], "archive_page_types": [ "category", @@ -257,7 +494,56 @@ "id": 30, "name": "Reader Activation Defaults" } - ] + ], + "user_input_fields": [ + { + "name": "heading", + "type": "string", + "label": "Heading", + "description": "A heading to describe the prompt.", + "required": true, + "default": "Support our work", + "max_length": 40 + }, + { + "name": "body", + "type": "string", + "label": "Body", + "description": "The primary call to action for the prompt.", + "required": true, + "default": "As an independent publication, we rely on contributions from readers like you to fund our journalism.", + "max_length": 300 + }, + { + "name": "thanks_message", + "type": "string", + "label": "Thanks Message", + "description": "A message to thank the reader for their contribution.", + "required": true, + "default": "Thanks for your contribution!", + "max_length": 300 + }, + { + "name": "button_label", + "type": "string", + "label": "Button Label", + "description": "The label for the primary CTA button.", + "required": true, + "default": "Contribute Now", + "max_length": 20 + } + ], + "help_info": { + "screenshot": "donation-inline.png", + "description": "The Inline Donation prompt is shown to all readers. It’s placed on every page below the content.", + "recommendations": [ + "Focus on the value readers add by contributing financially.", + "Highlight the importance of supporting your publication.", + "Use action-oriented verbs like “donate,” “give,” and “contribute.”", + "Create a sense of urgency. Encourage readers to take immediate action by using words like “now” and “today.”" + ], + "url": "https://help.newspack.com/engagement/reader-activation-system" + } } ], "segments": [ diff --git a/src/assets/donation-inline.png b/src/assets/donation-inline.png new file mode 100644 index 00000000..5cc6895f Binary files /dev/null and b/src/assets/donation-inline.png differ diff --git a/src/assets/donation-overlay.png b/src/assets/donation-overlay.png new file mode 100644 index 00000000..1f4d8849 Binary files /dev/null and b/src/assets/donation-overlay.png differ diff --git a/src/assets/newsletter-inline.png b/src/assets/newsletter-inline.png new file mode 100644 index 00000000..9c21caf1 Binary files /dev/null and b/src/assets/newsletter-inline.png differ diff --git a/src/assets/newsletter-overlay.png b/src/assets/newsletter-overlay.png new file mode 100644 index 00000000..0e6bdf7c Binary files /dev/null and b/src/assets/newsletter-overlay.png differ diff --git a/src/assets/reg-overlay.png b/src/assets/reg-overlay.png new file mode 100644 index 00000000..23c71bed Binary files /dev/null and b/src/assets/reg-overlay.png differ diff --git a/src/view/style.scss b/src/view/style.scss index ee86e4c9..c10fba4e 100644 --- a/src/view/style.scss +++ b/src/view/style.scss @@ -485,7 +485,8 @@ $width__tablet: 782px; display: none; } -.newspack-modal-checkout-open { +.newspack-modal-checkout-open, +.newspack-signin { .newspack-lightbox { display: none !important; } diff --git a/tests/test-presets.php b/tests/test-presets.php new file mode 100644 index 00000000..af9c815e --- /dev/null +++ b/tests/test-presets.php @@ -0,0 +1,151 @@ +assertTrue( isset( $presets['prompts'] ) && isset( $presets['segments'] ) && isset( $presets['campaigns'] ), 'The fetched presets match the JSON configuration.' ); + $this->assertEquals( 5, count( $presets['prompts'] ), 'The fetched presets match the JSON configuration.' ); + $this->assertEquals( 3, count( $presets['segments'] ), 'The fetched presets match the JSON configuration.' ); + $this->assertEquals( 1, count( $presets['campaigns'] ), 'The fetched presets match the JSON configuration.' ); + } + + /** + * Test fetching presets data with user inputs. + */ + public function test_preset_user_input() { + $user_inputs = [ + 'heading' => 'Test Heading copy', + 'body' => 'Test Body copy', + 'button_label' => 'Test Button Label', + 'success_message' => 'Test Success Message', + 'featured_image_id' => 123, + 'lists' => [ 1, 2, 3 ], + 'invalid_field' => 'invalid', + ]; + + // Test with invalid preset slug. + $this->assertTrue( \is_wp_error( Newspack_Popups_Presets::update_preset_prompt( 'invalid_slug', $user_inputs ) ), 'Invalid preset slug returns an error.' ); + + // Test with invalid field in user inputs. + $this->assertTrue( \is_wp_error( Newspack_Popups_Presets::update_preset_prompt( 'ras_registration_overlay', $user_inputs ) ), 'Invalid field name for a preset returns an error.' ); + + // Remove invalid field. + unset( $user_inputs['invalid_field'] ); + + // Test that data is updated with user inputs. + $presets = Newspack_Popups_Presets::update_preset_prompt( 'ras_registration_overlay', $user_inputs ); + $index = 0; + foreach ( $user_inputs as $field_name => $value ) { + $this->assertEquals( $value, $presets['prompts'][0]['user_input_fields'][ $index ]['value'], 'Preset data is returned with user inputs attached to each field.' ); + $index ++; + } + } + + /** + * Test activation of presets. Existing prompts and segments should be deactivated. + */ + public function test_preset_activation() { + $post_data = [ + 'post_title' => 'Preexisting Prompt', + 'post_content' => 'Preexisitng prompt body', + 'post_status' => 'publish', + 'post_type' => Newspack_Popups::NEWSPACK_POPUPS_CPT, + ]; + $preexisting_prompt = \wp_insert_post( $post_data ); + $preexisting_segment = [ + 'name' => 'Preexisting Segment', + 'configuration' => [ + 'is_subscribed' => true, + ], + ]; + Newspack_Popups_Segmentation::create_segment( $preexisting_segment ); + + // Activate presets. + $user_inputs = [ + 'heading' => 'Test Heading copy', + 'body' => 'Test Body copy', + 'button_label' => 'Test Button Label', + 'success_message' => 'Test Success Message', + 'featured_image_id' => 123, + 'lists' => [ 1, 2, 3 ], + ]; + $presets = Newspack_Popups_Presets::update_preset_prompt( 'ras_registration_overlay', $user_inputs ); + $activated = Newspack_Popups_Presets::activate_ras_presets(); + $all_prompts = Newspack_Popups_Model::retrieve_popups(); + + $preexisting_prompt_object = Newspack_Popups_Model::retrieve_popup_by_id( $preexisting_prompt, false, true ); + $this->assertEquals( + $preexisting_prompt_object['title'], + $post_data['post_title'] + ); + $this->assertEquals( + $preexisting_prompt_object['status'], + 'draft', + 'Preexisting prompt was deactivated' + ); + + $preset_titles = array_map( + function( $preset ) { + return $preset['title']; + }, + $presets['prompts'] + ); + $active_prompt_titles = array_map( + function( $prompt ) { + return $prompt['title']; + }, + $all_prompts + ); + $this->assertEmpty( array_diff( $preset_titles, $active_prompt_titles ) ); + $this->assertEquals( count( $presets['prompts'] ), count( $all_prompts ), 'Presets are the only published prompts' ); + + $all_segments = Newspack_Popups_Segmentation::get_segments(); + $this->assertEquals( $all_segments[0]['name'], $preexisting_segment['name'] ); + $this->assertTrue( $all_segments[0]['configuration']['is_disabled'], 'Preexisting segment is deactivated' ); + + $preset_segment_names = array_map( + function( $segment ) { + return $segment['name']; + }, + $presets['segments'] + ); + $activated_segment_names = array_map( + function( $segment ) { + return $segment['name']; + }, + array_filter( + $all_segments, + function( $segment ) { + return empty( $segment['options']['is_disabled'] ); + } + ) + ); + + $this->assertEmpty( array_diff( $preset_segment_names, $activated_segment_names ), 'Presets are the only active segments' ); + } +} diff --git a/tests/test-schemas.php b/tests/test-schemas.php index bf6940ce..9cc82f13 100644 --- a/tests/test-schemas.php +++ b/tests/test-schemas.php @@ -19,10 +19,11 @@ public function prompts_data() { return [ 'complete and valid' => [ [ - 'title' => 'Test Campaign', - 'content' => 'Test content', - 'status' => 'publish', - 'categories' => [ + 'slug' => 'test_campaign', + 'title' => 'Test Campaign', + 'content' => 'Test content', + 'status' => 'publish', + 'categories' => [ [ 'id' => 1, 'name' => 'Category 1', @@ -32,7 +33,7 @@ public function prompts_data() { 'name' => 'Category 2', ], ], - 'options' => [ + 'options' => [ 'background_color' => '#FFFFFF', 'display_title' => false, 'hide_border' => false, @@ -63,6 +64,44 @@ public function prompts_data() { 'duplicate_of' => 0, 'newspack_popups_has_disabled_popups' => false, ], + 'user_input_fields' => [ + [ + 'name' => 'heading', + 'type' => 'string', + 'label' => 'Heading', + 'description' => 'A heading to describe the prompt.', + 'required' => true, + 'default' => 'Sign Up', + 'max_length' => 40, + ], + [ + 'name' => 'body', + 'type' => 'string', + 'label' => 'Body', + 'description' => 'The primary call to action for the prompt.', + 'required' => true, + 'default' => 'Sign up for our free newsletter to receive the latest news.', + 'max_length' => 300, + ], + [ + 'name' => 'button_label', + 'type' => 'string', + 'label' => 'Button Label', + 'description' => 'The label for the primary CTA button.', + 'required' => true, + 'default' => 'Sign up', + 'max_length' => 20, + ], + [ + 'name' => 'success_message', + 'type' => 'string', + 'label' => 'Success Message', + 'description' => 'The message shown to readers after completing the signup.', + 'required' => true, + 'default' => 'Thank you for registering!', + 'max_length' => 300, + ], + ], ], true, ], diff --git a/tests/test-segments.php b/tests/test-segments.php new file mode 100644 index 00000000..e4997d9f --- /dev/null +++ b/tests/test-segments.php @@ -0,0 +1,533 @@ + 'Complete and valid', + 'priority' => 10, + 'configuration' => [ + 'max_posts' => 1, + 'min_posts' => 1, + 'min_session_posts' => 1, + 'max_session_posts' => 1, + 'is_subscribed' => true, + 'is_not_subscribed' => true, + 'is_donor' => true, + 'is_not_donor' => true, + 'is_former_donor' => true, + 'is_logged_in' => true, + 'is_not_logged_in' => true, + 'favorite_categories' => [], + 'referrers' => '', + 'referrers_not' => '', + 'is_disabled' => false, + ], + ]; + + /** + * A valid segment, without all the properties. + * + * @var array + */ + public $valid = [ + 'name' => 'Valid', + 'priority' => 20, + 'configuration' => [ + 'max_posts' => 1, + ], + ]; + + /** + * A segment missing required properties. + * + * @var array + */ + public $missing_required = [ + 'priority' => 30, + 'configuration' => [ + 'max_posts' => 1, + ], + ]; + + /** + * A segment with additional unknown properties. + * + * @var array + */ + public $additional_properties = [ + 'name' => 'Additional properties', + 'priority' => 40, + 'configuration' => [ + 'max_posts' => 1, + 'unknown' => 'invalid', + ], + ]; + + /** + * A segment with a string in a property that expects an integer. + * + * @var array + */ + public $invalid_int = [ + 'name' => 'Invalid Int', + 'priority' => 10, + 'configuration' => [ + 'max_posts' => 'string', + ], + ]; + + /** + * Make sure we have a clear environment + */ + public static function set_up_before_class() { + $segments = Newspack_Popups_Segmentation::get_segments(); + foreach ( $segments as $segment ) { + Newspack_Popups_Segmentation::delete_segment( $segment['id'] ); + } + } + + /** + * Data provider for test_create_segment + * + * @return array + */ + public function create_segment_data() { + return [ + 'complete_and_valid' => [ $this->complete_and_valid ], + 'valid' => [ $this->valid ], + 'missing_required' => [ $this->missing_required ], + 'additional_properties' => [ $this->additional_properties ], + 'invalid_int' => [ $this->invalid_int ], + ]; + } + + /** + * Test create_segment + * + * @param array $segment The segment. + * @dataProvider create_segment_data + */ + public function test_create_segment( $segment ) { + $result = Newspack_Popups_Segmentation::create_segment( $segment ); + $this->assertSame( 1, count( $result ) ); + + // Assert everything passed in was stored. + // As of today, any arbitrary properties are allowed. + foreach ( $segment as $key => $value ) { + $this->assertSame( $value, $result[0][ $key ] ); + } + + $created_properties = [ 'id', 'created_at', 'updated_at' ]; + foreach ( $created_properties as $property ) { + $this->assertArrayHasKey( $property, $result[0] ); + } + } + + /** + * Test create_segment throws an error when passed a string. + */ + public function test_create_segment_throws() { + $this->expectException( TypeError::class ); + Newspack_Popups_Segmentation::create_segment( 'string' ); + } + + /** + * Test get_segments + */ + public function test_get_segments() { + $this->assertSame( [], Newspack_Popups_Segmentation::get_segments() ); + Newspack_Popups_Segmentation::create_segment( $this->complete_and_valid ); + Newspack_Popups_Segmentation::create_segment( $this->valid ); + + $segments = Newspack_Popups_Segmentation::get_segments(); + + $this->assertSame( 2, count( $segments ) ); + $this->assertSame( $this->complete_and_valid['name'], $segments[0]['name'] ); + $this->assertSame( $this->valid['name'], $segments[1]['name'] ); + } + + /** + * Test get_segments fill in empty priorities. + */ + public function test_get_segments_reindex_priorities() { + $modified = $this->complete_and_valid; + unset( $modified['priority'] ); + Newspack_Popups_Segmentation::create_segment( $this->valid ); + Newspack_Popups_Segmentation::create_segment( $modified ); + + $segments = Newspack_Popups_Segmentation::get_segments(); + + $this->assertSame( 2, count( $segments ) ); + $this->assertSame( $this->valid['name'], $segments[0]['name'] ); + $this->assertSame( $this->complete_and_valid['name'], $segments[1]['name'] ); + $this->assertSame( 0, $segments[0]['priority'] ); + $this->assertSame( 1, $segments[1]['priority'] ); + } + + /** + * Test get_segments fill in empty priorities. + */ + public function test_get_segments_rremove_non_existent_categories() { + $cat_1 = $this->factory()->category->create_and_get( [ 'name' => 'Category 1' ] ); + $cat_2 = $this->factory()->category->create_and_get( [ 'name' => 'Category 2' ] ); + + + $modified = $this->complete_and_valid; + $modified['configuration']['favorite_categories'] = [ $cat_1->term_id, $cat_2->term_id, 9999 ]; + Newspack_Popups_Segmentation::create_segment( $modified ); + + $modified = $this->valid; + $modified['configuration']['favorite_categories'] = [ 8888 ]; + Newspack_Popups_Segmentation::create_segment( $modified ); + + $segments = Newspack_Popups_Segmentation::get_segments(); + + $this->assertSame( 2, count( $segments ) ); + $this->assertSame( [ $cat_1->term_id, $cat_2->term_id ], $segments[0]['configuration']['favorite_categories'] ); + $this->assertSame( [], $segments[1]['configuration']['favorite_categories'] ); + } + + /** + * Test get_segment_ids + */ + public function test_get_segment_ids() { + $this->assertSame( [], Newspack_Popups_Segmentation::get_segment_ids() ); + Newspack_Popups_Segmentation::create_segment( $this->complete_and_valid ); + Newspack_Popups_Segmentation::create_segment( $this->valid ); + + $segments = Newspack_Popups_Segmentation::get_segments(); + $segment_ids = Newspack_Popups_Segmentation::get_segment_ids(); + + $this->assertSame( 2, count( $segment_ids ) ); + $this->assertSame( $segments[0]['id'], $segment_ids[0] ); + $this->assertSame( $segments[1]['id'], $segment_ids[1] ); + } + + /** + * Test get_segment + */ + public function test_get_segment() { + $segments_to_create = [ + $this->complete_and_valid, + $this->valid, + $this->missing_required, + $this->additional_properties, + $this->invalid_int, + ]; + + foreach ( $segments_to_create as $segment ) { + Newspack_Popups_Segmentation::create_segment( $segment ); + } + + $segments = Newspack_Popups_Segmentation::get_segments(); + + foreach ( $segments as $segment ) { + $segment_id = $segment['id']; + $segment_from_db = Newspack_Popups_Segmentation::get_segment( $segment_id ); + $this->assertSame( $segment, $segment_from_db ); + } + + } + + /** + * Test delete_segment + */ + public function test_delete_segment() { + $segments_to_create = [ + $this->complete_and_valid, + $this->valid, + $this->missing_required, + $this->additional_properties, + $this->invalid_int, + ]; + + foreach ( $segments_to_create as $segment ) { + Newspack_Popups_Segmentation::create_segment( $segment ); + } + + $segments = Newspack_Popups_Segmentation::get_segments(); + + $delete_result = Newspack_Popups_Segmentation::delete_segment( $segments[0]['id'] ); + $this->assertSame( 4, count( $delete_result ) ); + $this->assertSame( $segments[1]['id'], $delete_result[0]['id'] ); + + $delete_result = Newspack_Popups_Segmentation::delete_segment( $segments[3]['id'] ); + $this->assertSame( 3, count( $delete_result ) ); + $this->assertSame( $segments[4]['id'], $delete_result[2]['id'] ); + + $delete_result2 = Newspack_Popups_Segmentation::delete_segment( 'non-existent' ); + $this->assertSame( $delete_result, $delete_result2 ); + + } + + /** + * Test update_segment + */ + public function test_update_segment() { + $segments_to_create = [ + $this->complete_and_valid, + $this->valid, + ]; + + foreach ( $segments_to_create as $segment ) { + Newspack_Popups_Segmentation::create_segment( $segment ); + } + + $segments = Newspack_Popups_Segmentation::get_segments(); + + $complete = $segments[0]; + + $complete['name'] = 'Edited'; + $complete['priority'] = 99; + $complete['configuration']['min_posts'] = 30; + $complete['other_properties'] = true; + + $result = Newspack_Popups_Segmentation::update_segment( $complete ); + + $this->assertSame( 'Edited', $result[0]['name'] ); + $this->assertSame( 30, $result[0]['configuration']['min_posts'] ); + $this->assertSame( 10, $result[0]['priority'], 'Priority should not be updated' ); + $this->assertNotContains( 'other_properties', $result[0], 'additional properties should not be included' ); + + $this->assertSame( $this->valid['name'], $result[1]['name'] ); + + } + + /** + * Test update_segment throws an error when passed a string. + */ + public function test_update_segment_throws() { + $this->expectException( TypeError::class ); + Newspack_Popups_Segmentation::update_segment( 'string' ); + } + + /** + * Test reindex_segments + */ + public function test_reindex_segments() { + $segments_to_create_in_different_order = [ + $this->complete_and_valid, + $this->additional_properties, + $this->invalid_int, + $this->valid, + ]; + + // Remove priority from the first segment. This will be populated with priority 0 and will be persisted when the second segment is created. + unset( $segments_to_create_in_different_order[0]['priority'] ); + + foreach ( $segments_to_create_in_different_order as $segment ) { + Newspack_Popups_Segmentation::create_segment( $segment ); + } + + $segments = Newspack_Popups_Segmentation::get_segments(); + + $reindexed = Newspack_Popups_Segmentation::reindex_segments( $segments ); + $index = 0; + foreach ( $reindexed as $segment ) { + $this->assertSame( $index, $segment['priority'] ); + $this->assertSame( $segments_to_create_in_different_order[ $index ]['name'], $segment['name'] ); + $index++; + } + + // Assert that the reindexed segments were not persisted. + $segments = Newspack_Popups_Segmentation::get_segments(); + $index = 0; + foreach ( $segments as $segment ) { + $expected_priority = $segments_to_create_in_different_order[ $index ]['priority'] ?? 0; + $this->assertSame( $segments_to_create_in_different_order[ $index ]['name'], $segment['name'] ); + $this->assertSame( $expected_priority, $segment['priority'] ); + $index++; + } + + } + + /** + * Test reindex_segments throws an error when passed a string. + */ + public function test_reindex_segments_throws() { + $this->expectException( TypeError::class ); + Newspack_Popups_Segmentation::reindex_segments( 'string' ); + } + + /** + * Test sort_segments + */ + public function test_sort_segments() { + $segments_to_create_in_different_order = [ + $this->complete_and_valid, + $this->additional_properties, + $this->invalid_int, + $this->valid, + ]; + + foreach ( $segments_to_create_in_different_order as $segment ) { + Newspack_Popups_Segmentation::create_segment( $segment ); + } + + $segments = Newspack_Popups_Segmentation::get_segments(); + + $new_order = [ + $segments[3]['id'], + $segments[0]['id'], + $segments[1]['id'], + $segments[2]['id'], + ]; + + $sorted = Newspack_Popups_Segmentation::sort_segments( $new_order ); + + $this->assertSame( $segments[3]['name'], $sorted[0]['name'] ); + $this->assertSame( $segments[0]['name'], $sorted[1]['name'] ); + $this->assertSame( $segments[1]['name'], $sorted[2]['name'] ); + $this->assertSame( $segments[2]['name'], $sorted[3]['name'] ); + + // Assert that the sorted segments were persisted. + $this->assertSame( $sorted, Newspack_Popups_Segmentation::get_segments() ); + + $this->assertTrue( + is_wp_error( + Newspack_Popups_Segmentation::sort_segments( array_merge( $new_order, [ 'asdasd' ] ) ) + ), + 'Should return wp error if an invalid id is part of the array' + ); + + } + + /** + * Test sort_segments throws an error when passed a string. + */ + public function test_sort_segments_throws() { + $this->expectException( TypeError::class ); + Newspack_Popups_Segmentation::sort_segments( 'string' ); + } + + /** + * Data provider for test_validate_segment_ids + * + * @return array + */ + public function data_validate_segment_ids() { + return [ + [ + [ 1, 2, 3 ], + [ + [ + 'id' => 1, + ], + [ + 'id' => 2, + ], + [ + 'id' => 3, + ], + ], + true, + ], + [ + [ 1, 2, 3 ], + [ + [ + 'id' => 1, + ], + [ + 'id' => 2, + ], + ], + false, + ], + [ + [ 1, 2, 3 ], + [ + [ + 'id' => 1, + ], + [ + 'id' => 2, + ], + [ + 'id' => 4, + ], + ], + false, + ], + [ + [ 1, 2, 3 ], + [ + [ + 'id' => 1, + ], + [ + 'id' => 2, + ], + [ + 'id' => 3, + ], + [ + 'id' => 4, + ], + ], + false, + ], + [ + [ 1, 2, 3, 4 ], + [ + [ + 'id' => 1, + ], + [ + 'id' => 2, + ], + [ + 'id' => 4, + ], + ], + false, + ], + [ + [ 1, 2 ], + 'string', + false, + true, + ], + [ + 'string', + [ + [ + 'id' => 1, + ], + ], + false, + true, + ], + ]; + } + + /** + * Test validate_segment_ids + * + * @param array $segment_ids Array of segment IDs to validate. + * @param array $segments Array of existing segments to validate against. + * @param boolean $expected Whether $segment_ids is valid. + * @param boolean $throw Whether it will throw Type_Error. + * @dataProvider data_validate_segment_ids + */ + public function test_validate_segment_ids( $segment_ids, $segments, $expected, $throw = false ) { + if ( $throw ) { + $this->expectException( TypeError::class ); + } + $this->assertSame( $expected, Newspack_Popups_Segmentation::validate_segment_ids( $segment_ids, $segments ) ); + } + +}