Changeset 3617733
- Timestamp:
- 07/21/2026 09:44:56 PM (3 months ago)
- Location:
- goultra-for-woocommerce
- Files:
-
- 77 added
- 29 edited
-
assets/screenshot-1.png (added)
-
assets/screenshot-2.png (added)
-
assets/screenshot-3.png (added)
-
assets/screenshot-4.png (added)
-
assets/screenshot-5.png (added)
-
assets/screenshot-6.png (added)
-
assets/screenshot-7.png (added)
-
assets/screenshot-8.png (added)
-
tags/3.9.24 (added)
-
tags/3.9.24/assets (added)
-
tags/3.9.24/assets/css (added)
-
tags/3.9.24/assets/css/admin.css (added)
-
tags/3.9.24/assets/css/chat-button.css (added)
-
tags/3.9.24/assets/css/index.php (added)
-
tags/3.9.24/assets/images (added)
-
tags/3.9.24/assets/images/goultra-logo.svg (added)
-
tags/3.9.24/assets/images/goultra-mark.svg (added)
-
tags/3.9.24/assets/images/index.php (added)
-
tags/3.9.24/assets/images/woo-order-placeholder.jpg (added)
-
tags/3.9.24/assets/index.php (added)
-
tags/3.9.24/assets/js (added)
-
tags/3.9.24/assets/js/admin.js (added)
-
tags/3.9.24/assets/js/campaigns.js (added)
-
tags/3.9.24/assets/js/checkout.js (added)
-
tags/3.9.24/assets/js/index.php (added)
-
tags/3.9.24/assets/js/subscribers.js (added)
-
tags/3.9.24/changelog.txt (added)
-
tags/3.9.24/goultra-for-woocommerce.php (added)
-
tags/3.9.24/includes (added)
-
tags/3.9.24/includes/class-goultra-abandoned-cart.php (added)
-
tags/3.9.24/includes/class-goultra-activator.php (added)
-
tags/3.9.24/includes/class-goultra-admin-verify.php (added)
-
tags/3.9.24/includes/class-goultra-analytics.php (added)
-
tags/3.9.24/includes/class-goultra-api.php (added)
-
tags/3.9.24/includes/class-goultra-campaigns.php (added)
-
tags/3.9.24/includes/class-goultra-chat-button.php (added)
-
tags/3.9.24/includes/class-goultra-checkout.php (added)
-
tags/3.9.24/includes/class-goultra-deactivator.php (added)
-
tags/3.9.24/includes/class-goultra-logs.php (added)
-
tags/3.9.24/includes/class-goultra-orders.php (added)
-
tags/3.9.24/includes/class-goultra-privacy.php (added)
-
tags/3.9.24/includes/class-goultra-rest-api.php (added)
-
tags/3.9.24/includes/class-goultra-settings.php (added)
-
tags/3.9.24/includes/class-goultra-shipping.php (added)
-
tags/3.9.24/includes/class-goultra-subscribers.php (added)
-
tags/3.9.24/includes/class-goultra-templates.php (added)
-
tags/3.9.24/includes/index.php (added)
-
tags/3.9.24/languages (added)
-
tags/3.9.24/languages/goultra-for-woocommerce-ar.mo (added)
-
tags/3.9.24/languages/goultra-for-woocommerce-ar.po (added)
-
tags/3.9.24/languages/goultra-for-woocommerce-es_ES.mo (added)
-
tags/3.9.24/languages/goultra-for-woocommerce-es_ES.po (added)
-
tags/3.9.24/languages/goultra-for-woocommerce-fr_FR.mo (added)
-
tags/3.9.24/languages/goultra-for-woocommerce-fr_FR.po (added)
-
tags/3.9.24/languages/goultra-for-woocommerce-he_IL.mo (added)
-
tags/3.9.24/languages/goultra-for-woocommerce-he_IL.po (added)
-
tags/3.9.24/languages/goultra-for-woocommerce.pot (added)
-
tags/3.9.24/languages/index.php (added)
-
tags/3.9.24/readme.txt (added)
-
tags/3.9.24/templates (added)
-
tags/3.9.24/templates/admin (added)
-
tags/3.9.24/templates/admin/campaigns-page.php (added)
-
tags/3.9.24/templates/admin/index.php (added)
-
tags/3.9.24/templates/admin/logs-page.php (added)
-
tags/3.9.24/templates/admin/settings-page.php (added)
-
tags/3.9.24/templates/admin/subscribers-page.php (added)
-
tags/3.9.24/templates/admin/template-presets.php (added)
-
tags/3.9.24/templates/admin/templates-page.php (added)
-
tags/3.9.24/templates/index.php (added)
-
tags/3.9.24/uninstall.php (added)
-
trunk/assets/css/admin.css (modified) (3 diffs)
-
trunk/assets/css/chat-button.css (added)
-
trunk/assets/js/admin.js (modified) (1 diff)
-
trunk/assets/js/campaigns.js (added)
-
trunk/assets/js/subscribers.js (modified) (3 diffs)
-
trunk/changelog.txt (added)
-
trunk/goultra-for-woocommerce.php (modified) (4 diffs)
-
trunk/includes/class-goultra-abandoned-cart.php (modified) (9 diffs)
-
trunk/includes/class-goultra-activator.php (modified) (1 diff)
-
trunk/includes/class-goultra-admin-verify.php (added)
-
trunk/includes/class-goultra-api.php (modified) (11 diffs)
-
trunk/includes/class-goultra-campaigns.php (added)
-
trunk/includes/class-goultra-chat-button.php (added)
-
trunk/includes/class-goultra-checkout.php (modified) (2 diffs)
-
trunk/includes/class-goultra-orders.php (modified) (3 diffs)
-
trunk/includes/class-goultra-rest-api.php (modified) (2 diffs)
-
trunk/includes/class-goultra-settings.php (modified) (12 diffs)
-
trunk/includes/class-goultra-shipping.php (modified) (1 diff)
-
trunk/includes/class-goultra-subscribers.php (modified) (2 diffs)
-
trunk/includes/class-goultra-templates.php (modified) (5 diffs)
-
trunk/languages/goultra-for-woocommerce-ar.mo (modified) ( previous)
-
trunk/languages/goultra-for-woocommerce-ar.po (modified) (1 diff)
-
trunk/languages/goultra-for-woocommerce-es_ES.mo (modified) ( previous)
-
trunk/languages/goultra-for-woocommerce-es_ES.po (modified) (1 diff)
-
trunk/languages/goultra-for-woocommerce-fr_FR.mo (modified) ( previous)
-
trunk/languages/goultra-for-woocommerce-fr_FR.po (modified) (1 diff)
-
trunk/languages/goultra-for-woocommerce-he_IL.mo (modified) ( previous)
-
trunk/languages/goultra-for-woocommerce-he_IL.po (modified) (2 diffs)
-
trunk/languages/goultra-for-woocommerce.pot (modified) (1 diff)
-
trunk/readme.txt (modified) (3 diffs)
-
trunk/templates/admin/campaigns-page.php (added)
-
trunk/templates/admin/settings-page.php (modified) (10 diffs)
-
trunk/templates/admin/subscribers-page.php (modified) (2 diffs)
-
trunk/templates/admin/template-presets.php (modified) (1 diff)
-
trunk/templates/admin/templates-page.php (modified) (25 diffs)
-
trunk/uninstall.php (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
-
goultra-for-woocommerce/trunk/assets/css/admin.css
r3603449 r3617733 209 209 background:var(--gu-slate-lt); padding:2px 8px; border-radius:20px; vertical-align:middle; 210 210 } 211 .gu-hdr-links { margin-left:auto; display:flex; gap: 6px; }211 .gu-hdr-links { margin-left:auto; display:flex; gap:2px; align-items:center; background:var(--gu-white); border:1px solid var(--gu-border); border-radius:12px; padding:4px; box-shadow:var(--gu-shadow-sm); } 212 212 .gu-hdr-links a { 213 font-size:12px; font-weight:600; color:var(--gu-slate); text-decoration:none; 214 padding:6px 13px; border:1px solid var(--gu-border); border-radius:8px; 213 display:inline-flex; align-items:center; gap:5px; 214 font-size:12.5px; font-weight:600; color:var(--gu-slate); text-decoration:none; 215 padding:6px 12px; border:0; border-radius:8px; white-space:nowrap; 215 216 transition:var(--gu-trans); 216 217 } 217 .gu-hdr-links a:hover { border-color:var(--gu-cyan); color:var(--gu-cyan); background:var(--gu-cyan-lt); } 218 .gu-hdr-links a:hover { color:var(--gu-navy); background:var(--gu-canvas); } 219 .gu-hdr-links a svg { opacity:.7; } 218 220 219 221 /* ── Cards ─────────────────────────────────────────────────────────────────── */ … … 559 561 .gu-ml-import-status { font-size:12px; font-weight:700; color:var(--gu-slate); } 560 562 .gu-ml-actions { display:flex; gap:8px; align-items:center; flex-wrap:wrap; margin-bottom:14px; } 561 .gu-ml-actions input[type="search"] { flex:1; min-width:220px; padding: 9px 12px; border:1px solid var(--gu-border); border-radius:10px; font-size:13px; }562 .gu-ml-actions select { padding: 9px 12px; border:1px solid var(--gu-border); border-radius:10px; font-size:13px; background:var(--gu-white); }563 .gu-ml-actions input[type="search"] { flex:1; min-width:220px; padding:7px 12px; height:36px; box-sizing:border-box; border:1px solid var(--gu-border); border-radius:9px; font-size:13px; } 564 .gu-ml-actions select { padding:7px 12px; height:36px; box-sizing:border-box; border:1px solid var(--gu-border); border-radius:9px; font-size:13px; background:var(--gu-white); } 563 565 .gu-ml-tablewrap { overflow-x:auto; } 564 566 .gu-ml-table { width:100%; border-collapse:collapse; } … … 582 584 .gu-live-pill { background:#DCFCE7; color:#166534; border-radius:20px; padding:2px 10px; font-size:11px; font-weight:700; white-space:nowrap; } 583 585 .gu-live-off { background:#F1F5F9; color:#94A3B8; border-radius:20px; padding:2px 10px; font-size:11px; font-weight:700; white-space:nowrap; } 586 587 /* ===== Custom select chevron (always visible, host-CSS-proof, RTL-aware) ===== */ 588 .gu-field select, .gu-ml-actions select, .gu-ac-timing select, #gu-set select, #gu-subs select, #gu-wiz select, #gu-camp select { 589 -webkit-appearance:none; -moz-appearance:none; appearance:none; 590 background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%2364748B' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E"); 591 background-repeat:no-repeat; 592 background-position:right 12px center; 593 background-size:12px 12px; 594 padding-right:34px !important; 595 } 596 #gu-set[dir="rtl"] select, #gu-subs[dir="rtl"] select, #gu-wiz[dir="rtl"] select, #gu-camp[dir="rtl"] select { 597 background-position:left 12px center; 598 padding-right:14px !important; 599 padding-left:34px !important; 600 } 601 602 /* ===== Campaigns page (#gu-camp) ===== */ 603 #gu-camp { direction:ltr; text-align:left; padding-left:20px; padding-right:20px; border-radius:8px; background:var(--gu-canvas); font-family:var(--gu-font); color:var(--gu-navy); } 604 #gu-camp[dir="rtl"] { direction:rtl; text-align:right; } 605 #gu-camp[dir="rtl"] .gu-hdr-links { margin-left:0; margin-right:auto; } 606 #gu-camp[dir="rtl"] .gu-hdr-ver { margin-left:0; margin-right:8px; } 607 #gu-camp[dir="rtl"] .gu-card-h p { padding-left:0; padding-right:42px; } 608 #gu-camp .gu-card { margin-bottom:20px; } 609 /* Cap the compose area so the form fields keep a readable width and the live 610 preview sits right beside them, instead of the form column stretching to the 611 full page width and pushing the preview far to the side. */ 612 .gu-camp-grid { display:grid; grid-template-columns:minmax(0,1fr) 340px; gap:40px; align-items:start; max-width:1080px; } 613 @media (max-width: 900px) { .gu-camp-grid { grid-template-columns:1fr; gap:24px; max-width:none; } } 614 .gu-camp-row { display:flex; gap:8px; align-items:center; flex-wrap:wrap; margin-bottom:8px; } 615 .gu-camp-sm { padding:8px 14px !important; font-size:12px !important; } 616 .gu-camp-msg { font-size:12px; font-weight:600; color:var(--gu-slate); } 617 .gu-camp-radio { display:flex; align-items:center; gap:6px; font-size:13px; font-weight:600; cursor:pointer; } 618 .gu-camp-reach { font-size:13px; font-weight:700; color:#166534; background:#DCFCE7; border-radius:10px; padding:8px 14px; display:inline-block; } 619 .gu-camp-reach.is-zero { color:#92400E; background:#FEF3C7; } 620 .gu-camp-var-row { display:flex; gap:8px; align-items:center; margin-bottom:8px; } 621 .gu-camp-var-n { font-family:Consolas,Menlo,monospace; font-size:12px; color:var(--gu-slate); min-width:40px; } 622 .gu-camp-var-row select { min-width:170px; } 623 .gu-camp-var-row input { flex:1; min-width:120px; padding:8px 10px; border:1px solid var(--gu-border); border-radius:8px; font-size:13px; } 624 #gc-products input[type="search"] { width:100%; padding:8px 12px; border:1px solid var(--gu-border); border-radius:8px; font-size:13px; margin:8px 0; } 625 .gu-camp-products { display:grid; grid-template-columns:repeat(auto-fill,minmax(84px,1fr)); gap:8px; max-height:220px; overflow-y:auto; padding:4px; } 626 .gu-camp-product { border:1px solid var(--gu-border); border-radius:10px; background:var(--gu-white); padding:6px; cursor:pointer; text-align:center; } 627 .gu-camp-product:hover { border-color:var(--gu-cyan); } 628 .gu-camp-product img { width:100%; height:64px; object-fit:cover; border-radius:6px; display:block; } 629 .gu-camp-product span { display:block; font-size:10px; color:var(--gu-navy); margin-top:4px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; } 630 #gc-header-url { width:100%; padding:9px 12px; border:1px solid var(--gu-border); border-radius:8px; font-size:12px; margin-top:8px; } 631 #gu-camp #gc-name { width:100%; padding:10px 12px; border:1px solid var(--gu-border); border-radius:9px; font-size:13px; } 632 .gu-camp-hist-tpl { color:var(--gu-slate); font-size:11px; } 633 #gu-cb-prod-q { width:100%; padding:9px 12px; border:1px solid var(--gu-border); border-radius:9px; font-size:12px; margin-bottom:8px; } 634 .gu-camp-chosen { align-items:center; gap:10px; margin-top:10px; padding:8px; border:1px solid var(--gu-border); border-radius:10px; background:var(--gu-canvas); } 635 .gu-camp-chosen img { width:44px; height:44px; object-fit:cover; border-radius:8px; flex:none; background:#E4E9EC; } 636 .gu-camp-chosen-txt { font-size:12.5px; font-weight:600; color:var(--gu-navy); flex:1; min-width:0; } 637 .gu-camp-chosen-x { border:0; background:transparent; color:#dc2626; font-size:12px; font-weight:700; cursor:pointer; padding:4px 8px; border-radius:6px; } 638 .gu-camp-chosen-x:hover { background:#FEE2E2; } 639 .gu-camp-link { display:inline-block; margin-top:8px; font-size:12px; color:var(--gu-cyan); text-decoration:none; font-weight:600; cursor:pointer; } 640 .gu-camp-link:hover { text-decoration:underline; } 641 /* Admin-number verification (WhatsApp OTP) */ 642 .gu-av { margin:0 0 14px; padding:14px; border:1px solid #E0F2FE; background:#F0F9FF; border-radius:12px; } 643 .gu-av-head { display:flex; gap:10px; align-items:flex-start; margin-bottom:10px; } 644 .gu-av-ico { flex:none; width:34px; height:34px; border-radius:9px; background:#fff; border:1px solid #BAE6FD; display:flex; align-items:center; justify-content:center; } 645 .gu-av-title { font-weight:700; color:var(--gu-navy); font-size:13.5px; margin-bottom:2px; } 646 .gu-av-start, .gu-av-codewrap, .gu-av-done { display:flex; align-items:center; gap:8px; flex-wrap:wrap; } 647 .gu-av-start input, .gu-av-codewrap input { padding:9px 12px; border:1px solid var(--gu-border); border-radius:9px; font-size:13px; margin:0; } 648 .gu-av-start #gu-av-phone { flex:1; min-width:180px; } 649 .gu-av-codewrap #gu-av-code { width:130px; letter-spacing:4px; text-align:center; font-weight:700; } 650 .gu-av-codewrap a { font-size:12px; color:var(--gu-cyan); text-decoration:none; font-weight:600; } 651 .gu-av-badge { font-weight:700; color:#166534; background:#DCFCE7; border:1px solid #BBF7D0; border-radius:20px; padding:4px 12px; font-size:12.5px; } 652 .gu-av-done bdi { font-weight:600; color:var(--gu-navy); font-variant-numeric:tabular-nums; } 653 .gu-av-msg { font-size:12.5px; margin-top:8px; } 654 .gu-av-msg.ok { color:#166534; } 655 .gu-av-msg.bad { color:#dc2626; } 656 /* phone mock - WhatsApp-style preview */ 657 .gu-camp-phone { border-radius:26px; overflow:hidden; box-shadow:0 12px 32px rgba(13,28,44,.16), 0 0 0 1px rgba(13,28,44,.06); background:#EFEAE2; } 658 .gu-camp-phone-top { display:flex; align-items:center; gap:10px; background:#008069; color:#fff; padding:12px 14px; } 659 .gu-camp-avatar, .gu-camp-avatar-img { width:36px; height:36px; border-radius:50%; flex:none; } 660 .gu-camp-avatar { background:#fff3; color:#fff; display:flex; align-items:center; justify-content:center; font-weight:800; font-size:16px; } 661 .gu-camp-avatar-img { object-fit:cover; background:#fff; } 662 .gu-camp-phone-id { flex:1; min-width:0; line-height:1.25; } 663 .gu-camp-phone-id b { display:block; font-size:13.5px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; } 664 .gu-camp-phone-id small { font-size:11px; opacity:.8; } 665 .gu-camp-phone-dots { flex:none; display:flex; } 666 .gu-camp-phone-bg { padding:14px 12px 24px; min-height:280px; background-color:#EFEAE2; background-image:radial-gradient(rgba(13,28,44,.045) 1px, transparent 1px); background-size:16px 16px; } 667 .gu-camp-day { width:max-content; margin:0 auto 12px; background:#fff; color:#54656F; font-size:11px; font-weight:600; padding:4px 12px; border-radius:8px; box-shadow:0 1px 1px rgba(0,0,0,.06); } 668 .gu-camp-bubble { position:relative; background:#fff; border-radius:10px; padding:6px; max-width:94%; box-shadow:0 1px 1px rgba(0,0,0,.10); font-size:13.5px; color:#111B21; } 669 .gu-camp-bubble::before { content:''; position:absolute; top:0; inset-inline-start:-7px; border:8px solid transparent; border-top-color:#fff; border-inline-end-color:#fff; } 670 .gu-camp-b-empty { color:#8696A0; font-size:12px; text-align:center; padding:22px 10px; } 671 .gu-camp-b-media { position:relative; width:100%; aspect-ratio:1.9/1; min-height:112px; background:#E4E9EC; border-radius:8px; overflow:hidden; display:flex; align-items:center; justify-content:center; margin-bottom:7px; } 672 .gu-camp-b-media-ico { color:#8F9DA8; display:flex; } 673 .gu-camp-b-media.has-img .gu-camp-b-media-ico { display:none; } 674 .gu-camp-b-media-img { position:absolute; inset:0; width:100%; height:100%; object-fit:cover; display:block; } 675 .gu-camp-b-body { padding:3px 5px; line-height:1.5; white-space:normal; word-wrap:break-word; } 676 .gu-camp-b-var { background:#D9FDD3; border-radius:4px; padding:0 3px; font-weight:600; } 677 .gu-camp-b-footer { padding:5px 5px 0; font-size:11px; color:#8696A0; } 678 .gu-camp-b-time { display:flex; align-items:center; justify-content:flex-end; gap:3px; font-size:10.5px; color:#8696A0; padding:3px 5px 5px; } 679 .gu-camp-b-btn { display:flex; align-items:center; justify-content:center; gap:6px; border-top:1px solid #E9EDEF; color:#00A5F4; padding:9px 4px 5px; font-size:13.5px; font-weight:600; } 680 681 /* recipient picker */ 682 .gu-camp-rcp-head { display:flex; gap:8px; align-items:center; margin-bottom:8px; } 683 .gu-camp-rcp-head input[type="search"] { flex:1; padding:9px 12px; border:1px solid var(--gu-border); border-radius:9px; font-size:13px; margin:0; height:auto; } 684 .gu-camp-rcp-chip { flex:none; font-size:11.5px; font-weight:700; color:var(--gu-slate); background:var(--gu-canvas); border:1px solid var(--gu-border); border-radius:20px; padding:5px 12px; white-space:nowrap; } 685 .gu-camp-rcp-chip.is-on { color:#166534; background:#DCFCE7; border-color:#BBF7D0; } 686 .gu-camp-rcp-tools { display:flex; flex-wrap:wrap; align-items:center; gap:8px; margin-bottom:8px; font-size:12px; color:var(--gu-border); } 687 .gu-camp-rcp-tools a { color:var(--gu-cyan); text-decoration:none; font-weight:600; } 688 /* The self-test tip sits on its own full-width line under the Select-all/Clear 689 links and reads in the document direction (right-aligned in Hebrew/Arabic), 690 instead of being squeezed to the side and left-aligned. */ 691 .gu-camp-rcp-hint { flex-basis:100%; color:var(--gu-slate); font-size:11px; margin:4px 0 0; max-width:none; text-align:start; line-height:1.5; } 692 .gu-camp-rcp-list { max-height:280px; overflow-y:auto; border:1px solid var(--gu-border); border-radius:11px; background:var(--gu-white); } 693 .gu-camp-rcp-empty { padding:18px; text-align:center; color:var(--gu-slate); font-size:12px; } 694 #gu-camp .gu-camp-rcp-item { display:flex; align-items:center; gap:11px; padding:9px 12px; margin:0; font-size:13px; font-weight:400; cursor:pointer; border-bottom:1px solid var(--gu-canvas); } 695 #gu-camp .gu-camp-rcp-item:last-child { border-bottom:0; } 696 #gu-camp .gu-camp-rcp-item:hover { background:var(--gu-canvas); } 697 #gu-camp .gu-camp-rcp-item.is-checked { background:#F0FDFA; } 698 #gu-camp .gu-camp-rcp-item { user-select:none; -webkit-user-select:none; } 699 #gu-camp .gu-camp-rcp-item:focus-visible { outline:2px solid var(--gu-cyan); outline-offset:-2px; } 700 #gu-camp .gu-camp-rcp-cb { width:18px; height:18px; flex:none; border:2px solid #CBD5E1; border-radius:5px; position:relative; box-sizing:border-box; transition:background .12s ease, border-color .12s ease; } 701 #gu-camp .gu-camp-rcp-item.is-checked .gu-camp-rcp-cb { background:var(--gu-cyan); border-color:var(--gu-cyan); } 702 #gu-camp .gu-camp-rcp-item.is-checked .gu-camp-rcp-cb::after { content:''; position:absolute; left:5px; top:1px; width:4px; height:9px; border:solid #fff; border-width:0 2px 2px 0; transform:rotate(45deg); } 703 .gu-camp-rcp-ava { width:30px; height:30px; border-radius:50%; background:#E8EDF2; color:#475569; display:flex; align-items:center; justify-content:center; font-size:12px; font-weight:700; flex:none; } 704 .gu-camp-rcp-meta { display:flex; flex-direction:column; min-width:0; line-height:1.3; } 705 .gu-camp-rcp-name { color:var(--gu-navy); font-weight:600; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; } 706 .gu-camp-rcp-noname { color:var(--gu-slate); font-weight:400; font-style:italic; } 707 .gu-camp-rcp-phone { color:var(--gu-slate); font-size:12px; font-variant-numeric:tabular-nums; } 708 /* campaign radios */ 709 #gu-camp .gu-camp-radio { display:inline-flex; align-items:center; gap:7px; } 710 #gu-camp .gu-camp-radio input[type="radio"] { width:16px; height:16px; margin:0; accent-color:var(--gu-cyan); } 711 /* scheduling */ 712 .gu-camp-sched { display:flex; align-items:flex-end; gap:12px; flex-wrap:wrap; margin-top:4px; } 713 #gu-camp .gu-camp-sched-f { display:inline-flex; flex-direction:column; gap:4px; margin:0; font-size:11px; font-weight:700; color:var(--gu-slate); } 714 #gu-camp .gu-camp-sched-f span { display:block; } 715 .gu-camp-sched-f input { padding:8px 10px; border:1px solid var(--gu-border); border-radius:8px; font-size:13px; font-family:var(--gu-font); } 716 .gu-camp-sched-f input[type="time"] { width:130px; } 717 .gu-camp-sched-f input[type="date"] { width:160px; } 718 .gu-camp-sched-note { font-size:12px; color:#166534; font-weight:600; padding-bottom:8px; } 719 720 /* Marketing List: add-my-number mini form */ 721 .gu-ml-add { background:var(--gu-canvas); border:1px solid var(--gu-border); border-radius:12px; padding:14px 16px; margin-bottom:14px; } 722 .gu-ml-add-fields { display:flex; gap:8px; align-items:center; flex-wrap:wrap; } 723 .gu-ml-add-fields input { padding:9px 12px; border:1px solid var(--gu-border); border-radius:8px; font-size:13px; background:var(--gu-white); } 724 #gu-subs-add-name { min-width:180px; } 725 #gu-subs-add-phone { min-width:180px; } 726 727 /* Marketing List: inline name edit */ 728 .gu-ml-namecell { white-space:nowrap; } 729 .gu-ml-name { display:inline-block; vertical-align:middle; } 730 .gu-ml-edit { background:none; border:0; cursor:pointer; color:var(--gu-slate); padding:2px 4px; margin-inline-start:6px; border-radius:6px; vertical-align:middle; opacity:0; transition:var(--gu-trans); } 731 tr:hover .gu-ml-edit { opacity:1; } 732 .gu-ml-edit:hover { color:var(--gu-cyan); background:var(--gu-canvas); } 733 .gu-ml-editbox { display:inline-flex; align-items:center; gap:4px; } 734 .gu-ml-name-input { padding:5px 9px; border:1px solid var(--gu-cyan); border-radius:7px; font-size:13px; min-width:140px; } 735 .gu-ml-name-save, .gu-ml-name-cancel { border:0; border-radius:6px; width:26px; height:28px; cursor:pointer; font-size:13px; font-weight:700; } 736 .gu-ml-name-save { background:var(--gu-cyan); color:#fff; } 737 .gu-ml-name-cancel { background:var(--gu-canvas); color:var(--gu-slate); } 738 739 /* ===== Rich custom/marketing template builder (#gu-custom-editor .gu-cb) ===== */ 740 .gu-cb-grid { display:grid; grid-template-columns:minmax(0,1fr) 340px; gap:28px; align-items:start; } 741 @media (max-width:900px){ .gu-cb-grid { grid-template-columns:1fr; } } 742 .gu-cb-field { margin-bottom:14px; } 743 .gu-cb-field > label { display:block; font-size:13px; font-weight:700; color:var(--gu-navy); margin-bottom:6px; } 744 .gu-cb-opt { font-weight:400; color:var(--gu-slate); font-size:12px; } 745 .gu-cb-hint { font-size:12px; color:var(--gu-slate); margin:5px 0 0; } 746 .gu-cb-form input[type="text"], .gu-cb-form textarea, .gu-cb-form select { width:100%; padding:10px 12px; border:1px solid var(--gu-border); border-radius:9px; font-size:14px; font-family:var(--gu-font); box-sizing:border-box; background:var(--gu-white); color:var(--gu-navy); } 747 .gu-cb-form textarea { line-height:1.6; resize:vertical; min-height:120px; } 748 .gu-cb-row2 { display:grid; grid-template-columns:1fr 1fr; gap:12px; } 749 .gu-cb-warn { padding:10px 14px; border-radius:9px; font-size:13px; line-height:1.5; margin-bottom:14px; background:#FEF2F2; border:1px solid #FECACA; color:#991B1B; } 750 .gu-cb-warn-amber { background:#FFFBEB; border-color:#FDE68A; color:#92400E; } 751 .gu-cb-chips { display:flex; flex-wrap:wrap; gap:6px; } 752 .gu-cb-chip { cursor:pointer; font-size:12px; background:#EFF6FF; color:#1D4ED8; border:1px solid #DBEAFE; border-radius:6px; padding:3px 8px; } 753 .gu-cb-chip:hover { background:#DBEAFE; } 754 .gu-cb-optout { background:#FFFBEB; border:1px solid #FDE68A; border-radius:10px; padding:12px 14px; margin-bottom:14px; } 755 .gu-cb-optout > label:first-child { display:block; font-size:13px; font-weight:700; color:#92400E; margin-bottom:8px; } 756 .gu-cb-radio { display:flex; gap:7px; align-items:center; font-size:13px; cursor:pointer; margin-top:4px; } 757 .gu-cb-radio input { accent-color:var(--gu-cyan); } 758 .gu-cb-btnrow { border:1px solid var(--gu-border); border-radius:9px; padding:8px; margin-bottom:8px; background:var(--gu-canvas); } 759 .gu-cb-btnrow-top { display:flex; gap:8px; align-items:center; } 760 .gu-cb-btnrow-top .gu-cb-bt { flex:0 0 130px; } 761 .gu-cb-btnrow-top input { flex:1; } 762 .gu-cb-btnrm { flex:none; width:30px; height:34px; border:0; border-radius:7px; background:#FEF2F2; color:#991B1B; cursor:pointer; font-weight:700; } 763 .gu-cb-bv { margin-top:6px; } 764 .gu-cb-msg { margin-top:12px; padding:11px 14px; border-radius:9px; font-size:13px; } 765 .gu-cb-msg.ok { background:#DCFCE7; color:#166534; border:1px solid #BBF7D0; } 766 .gu-cb-msg.bad { background:#FEF2F2; color:#991B1B; border:1px solid #FECACA; } 767 .gu-cb-preview { position:sticky; top:20px; } 768 .gu-camp-b-htext { font-weight:700; padding:6px 5px 0; color:#111B21; } -
goultra-for-woocommerce/trunk/assets/js/admin.js
r3588005 r3617733 274 274 275 275 276 277 // ── Test send ──278 window.guTestSend = function() {279 var btn = document.getElementById('gu-test-btn');280 var result = document.getElementById('gu-test-result');281 var phone = (document.getElementById('gu-test-phone').value || '').trim();282 var orderId = (document.getElementById('gu-test-order').value || '').trim();283 if (!phone) { result.textContent = 'Enter a phone number'; result.style.color='#b91c1c'; return; }284 btn.disabled = true;285 btn.textContent = 'Sending...';286 result.textContent = '';287 var GU = window.goUltraAdmin || {};288 $.post(GU.ajaxUrl, {289 action: 'goultra_test_send',290 nonce: GU.nonce,291 phone: phone,292 order_id: orderId293 }, function(res) {294 btn.disabled = false;295 btn.innerHTML = '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg> Test';296 if (res.success) {297 var d = res.data;298 var inner = d.response || {};299 // Check if GoUltra actually succeeded in sending to Meta300 var goSuccess = inner.success;301 var goCode = inner.code;302 var goResp = inner.response || {};303 var metaError = goResp.error || null;304 if (metaError) {305 result.style.color = '#b91c1c';306 result.textContent = '❌ Meta error: ' + metaError.message + ' (code ' + metaError.code + ')';307 } else if (goSuccess === false) {308 result.style.color = '#b91c1c';309 result.textContent = '❌ GoUltra error: ' + JSON.stringify(d.response);310 } else {311 result.style.color = '#15803d';312 result.textContent = '✅ Sent to ' + (d.phone_sent || phone) + '. GoUltra response: ' + JSON.stringify(d.response);313 }314 } else {315 result.style.color = '#b91c1c';316 result.textContent = '❌ ' + JSON.stringify(res.data);317 }318 }).fail(function(xhr) {319 btn.disabled = false;320 btn.innerHTML = '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg> Test';321 result.style.color = '#b91c1c';322 result.textContent = 'AJAX error: ' + xhr.status;323 });324 };325 326 276 })(jQuery); -
goultra-for-woocommerce/trunk/assets/js/subscribers.js
r3603449 r3617733 22 22 if ( src === 'woo_abandoned_cart' ) { return esc( S.i18n.cart ); } 23 23 return src ? esc( src ) : ''; 24 } 25 26 // consented_at may arrive as a unix timestamp (seconds or ms) or an ISO string. 27 function formatDate( v ) { 28 if ( v == null || v === '' ) { return ''; } 29 var s = String( v ); 30 if ( /^\d{9,13}$/.test( s ) ) { 31 var ms = s.length >= 13 ? parseInt( s, 10 ) : parseInt( s, 10 ) * 1000; 32 var d = new Date( ms ); 33 if ( ! isNaN( d.getTime() ) ) { return d.toLocaleDateString(); } 34 return ''; 35 } 36 return s.substring( 0, 10 ); 24 37 } 25 38 … … 63 76 ? '<span class="gu-ml-badge gu-ml-badge-on">' + esc( S.i18n.subscribed ) + '</span>' 64 77 : '<span class="gu-ml-badge gu-ml-badge-off">' + esc( S.i18n.unsubscribed ) + '</span>'; 65 var date = s.consented_at ? esc( String( s.consented_at ).substring( 0, 10 ) ) : '';78 var date = esc( formatDate( s.consented_at ) ); 66 79 var action = granted 67 80 ? '<button type="button" class="gu-btn gu-btn-d gu-subs-unsub" style="padding:5px 12px;font-size:11px;" data-phone="' + esc( s.phone ) + '">' + esc( S.i18n.unsub ) + '</button>' 68 81 : ''; 69 82 html += '<tr>' 70 + '<td>' + esc( s.name ) + '</td>' 83 + '<td class="gu-ml-namecell" data-phone="' + esc( s.phone ) + '">' 84 + '<span class="gu-ml-name">' + esc( s.name || '' ) + '</span>' 85 + '<button type="button" class="gu-ml-edit" title="' + esc( S.i18n.editName ) + '" aria-label="' + esc( S.i18n.editName ) + '">' 86 + '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.12 2.12 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>' 87 + '</button>' 88 + '</td>' 71 89 + '<td class="gu-ml-phone"><bdi dir="ltr">' + esc( s.phone ) + '</bdi></td>' 72 90 + '<td>' + esc( s.email ) + '</td>' … … 149 167 } ); 150 168 169 // ── edit a subscriber's name (name only, synced to GoUltra) ────────────── 170 $( '#gu-subs-body' ).on( 'click', '.gu-ml-edit', function () { 171 var cell = $( this ).closest( '.gu-ml-namecell' ); 172 if ( cell.find( 'input' ).length ) { return; } 173 var current = $.trim( cell.find( '.gu-ml-name' ).text() ); 174 var phone = cell.data( 'phone' ); 175 cell.data( 'orig', current ); 176 cell.html( 177 '<span class="gu-ml-editbox">' 178 + '<input type="text" class="gu-ml-name-input" value="' + esc( current ) + '" />' 179 + '<button type="button" class="gu-ml-name-save" title="' + esc( S.i18n.save ) + '">✓</button>' 180 + '<button type="button" class="gu-ml-name-cancel" title="' + esc( S.i18n.cancel ) + '">✕</button>' 181 + '</span>' 182 ).data( 'phone', phone ); 183 cell.find( 'input' ).focus().select(); 184 } ); 185 $( '#gu-subs-body' ).on( 'click', '.gu-ml-name-cancel', function () { 186 var cell = $( this ).closest( '.gu-ml-namecell' ); 187 redrawName( cell, cell.data( 'orig' ) ); 188 } ); 189 $( '#gu-subs-body' ).on( 'keydown', '.gu-ml-name-input', function ( e ) { 190 if ( e.key === 'Enter' ) { $( this ).siblings( '.gu-ml-name-save' ).click(); } 191 if ( e.key === 'Escape' ) { $( this ).siblings( '.gu-ml-name-cancel' ).click(); } 192 } ); 193 $( '#gu-subs-body' ).on( 'click', '.gu-ml-name-save', function () { 194 var cell = $( this ).closest( '.gu-ml-namecell' ); 195 var phone = cell.data( 'phone' ); 196 var name = $.trim( cell.find( 'input' ).val() || '' ); 197 var save = $( this ).prop( 'disabled', true ); 198 save.text( '…' ); 199 $.post( S.ajaxUrl, { action: 'goultra_edit_subscriber_name', nonce: S.nonce, phone: phone, name: name } ) 200 .done( function ( res ) { 201 if ( res && res.success ) { 202 redrawName( cell, name ); 203 } else { 204 save.prop( 'disabled', false ).html( '✓' ); 205 window.alert( res && res.data && res.data.message ? res.data.message : S.i18n.error ); 206 } 207 } ) 208 .fail( function () { 209 save.prop( 'disabled', false ).html( '✓' ); 210 window.alert( S.i18n.error ); 211 } ); 212 } ); 213 function redrawName( cell, name ) { 214 cell.html( 215 '<span class="gu-ml-name">' + esc( name || '' ) + '</span>' 216 + '<button type="button" class="gu-ml-edit" title="' + esc( S.i18n.editName ) + '" aria-label="' + esc( S.i18n.editName ) + '">' 217 + '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.12 2.12 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>' 218 + '</button>' 219 ); 220 } 221 222 // ── add my number (manual contact for self-testing campaigns) ──────────── 223 $( '#gu-subs-add-btn' ).on( 'click', function () { 224 $( '#gu-subs-add' ).slideToggle( 120 ); 225 } ); 226 $( '#gu-subs-add-save' ).on( 'click', function () { 227 var btn = $( this ); 228 var phone = $.trim( $( '#gu-subs-add-phone' ).val() || '' ); 229 var msg = $( '#gu-subs-add-msg' ).text( '' ); 230 if ( ! phone ) { msg.text( S.i18n.phoneRequired ); return; } 231 btn.prop( 'disabled', true ); 232 msg.text( S.i18n.adding ); 233 $.post( S.ajaxUrl, { 234 action: 'goultra_add_subscriber', 235 nonce: S.nonce, 236 phone: phone, 237 name: $.trim( $( '#gu-subs-add-name' ).val() || '' ) 238 } ).done( function ( res ) { 239 btn.prop( 'disabled', false ); 240 if ( res && res.success ) { 241 msg.text( S.i18n.added ); 242 setTimeout( function () { $( '#gu-subs-add' ).slideUp( 120 ); msg.text( '' ); }, 1400 ); 243 offset = 0; 244 load(); 245 } else { 246 msg.text( res && res.data && res.data.message ? res.data.message : S.i18n.addError ); 247 } 248 } ).fail( function () { 249 btn.prop( 'disabled', false ); 250 msg.text( S.i18n.addError ); 251 } ); 252 } ); 253 151 254 // ── list settings (name source + email) ────────────────────────────────── 152 255 $( '#gu-ml-save' ).on( 'click', function () { -
goultra-for-woocommerce/trunk/goultra-for-woocommerce.php
r3601733 r3617733 1 1 <?php 2 2 /** 3 * Plugin Name: GoUltra for WooCommerce 3 * Plugin Name: GoUltra for WooCommerce - WhatsApp Marketing, Notifications & Abandoned Cart 4 4 * Plugin URI: https://wordpress.org/plugins/goultra-for-woocommerce/ 5 * Description: Connect your WooCommerce store to GoUltra's WhatsApp Business Platform. Send automated order confirmations, shipping updates, abandoned cart recovery messages, and more - all via WhatsApp.6 * Version: 3. 8.15 * Description: WooCommerce WhatsApp notifications: automated order, shipping and abandoned-cart messages, plus opt-in marketing campaigns, powered by GoUltra. 6 * Version: 3.9.24 7 7 * Requires at least: 6.0 8 8 * Requires PHP: 7.4 … … 25 25 26 26 // Plugin constants 27 define( 'GOULTRA_WC_VERSION', '3. 8.1' );27 define( 'GOULTRA_WC_VERSION', '3.9.24' ); 28 28 define( 'GOULTRA_WC_PLUGIN_FILE', __FILE__ ); 29 29 define( 'GOULTRA_WC_PLUGIN_DIR', plugin_dir_path( __FILE__ ) ); … … 167 167 require_once GOULTRA_WC_PLUGIN_DIR . 'includes/class-goultra-privacy.php'; 168 168 require_once GOULTRA_WC_PLUGIN_DIR . 'includes/class-goultra-subscribers.php'; 169 require_once GOULTRA_WC_PLUGIN_DIR . 'includes/class-goultra-campaigns.php'; 170 require_once GOULTRA_WC_PLUGIN_DIR . 'includes/class-goultra-admin-verify.php'; 171 require_once GOULTRA_WC_PLUGIN_DIR . 'includes/class-goultra-chat-button.php'; 169 172 170 173 // DB migrations … … 220 223 GoUltra_Privacy::instance(); // GDPR export/erase + privacy policy (always on) 221 224 GoUltra_Subscribers::instance(); // Marketing warm-list feed + admin screen 225 GoUltra_Campaigns::instance(); // Marketing campaigns (compose/send/history) 226 GoUltra_Admin_Verify::instance(); // Admin-number WhatsApp OTP verification 227 GoUltra_Chat_Button::instance(); // Free front-end WhatsApp click-to-chat button 222 228 } 223 229 add_action( 'plugins_loaded', 'goultra_wc_init' ); -
goultra-for-woocommerce/trunk/includes/class-goultra-abandoned-cart.php
r3599583 r3617733 115 115 if ( get_transient( 'goultra_sched_ac' ) ) return; 116 116 117 // Pass $unique = true so Action Scheduler itself refuses a duplicate: two 118 // concurrent "init" requests racing past the as_has_scheduled_action() check 119 // (e.g. the first traffic right after activation) can no longer each register 120 // a recurring scanner that would then fire forever in parallel. 117 121 if ( ! as_has_scheduled_action( 'goultra_scan_abandoned_carts', array(), self::AS_GROUP ) ) { 118 as_schedule_recurring_action( time(), 5 * MINUTE_IN_SECONDS, 'goultra_scan_abandoned_carts', array(), self::AS_GROUP );122 as_schedule_recurring_action( time(), 5 * MINUTE_IN_SECONDS, 'goultra_scan_abandoned_carts', array(), self::AS_GROUP, true ); 119 123 } 120 124 if ( ! as_has_scheduled_action( 'goultra_cleanup_old_carts', array(), self::AS_GROUP ) ) { 121 as_schedule_recurring_action( time(), DAY_IN_SECONDS, 'goultra_cleanup_old_carts', array(), self::AS_GROUP );125 as_schedule_recurring_action( time(), DAY_IN_SECONDS, 'goultra_cleanup_old_carts', array(), self::AS_GROUP, true ); 122 126 } 123 127 … … 131 135 public function scan_and_schedule() { 132 136 if ( ! function_exists( 'as_schedule_single_action' ) ) return; 133 if ( 'yes' !== get_option( GOULTRA_WC_OPTION_PREFIX . 'enable_abandoned_cart', ' yes' ) ) return;137 if ( 'yes' !== get_option( GOULTRA_WC_OPTION_PREFIX . 'enable_abandoned_cart', 'no' ) ) return; 134 138 135 139 global $wpdb; … … 157 161 158 162 foreach ( $carts as $cart ) { 163 // Atomically CLAIM the cart before scheduling: flip sent_to_goultra 0 -> 1 164 // in a single conditional UPDATE and only proceed if THIS query changed the 165 // row. Two overlapping scanner runs (wp-cron + an instant queue kick) can 166 // both SELECT the same cart, but only one wins the claim, so a cart can 167 // never receive two full step sequences (the owner's duplicate-send worry). 168 $claimed = $wpdb->query( 169 $wpdb->prepare( 170 "UPDATE {$table} SET sent_to_goultra = 1 WHERE id = %d AND sent_to_goultra = 0", 171 (int) $cart->id 172 ) 173 ); 174 if ( 1 !== (int) $claimed ) { 175 continue; // another runner already claimed this cart 176 } 159 177 $this->schedule_sequence( $cart ); 160 178 } … … 218 236 ); 219 237 220 // Warm list: a consented shopper now being marketed to is a subscriber. 221 // Recorded once (each cart is scheduled once). Non-blocking / never fatal. 222 if ( class_exists( 'GoUltra_Subscribers' ) ) { 223 try { 224 GoUltra_Subscribers::instance()->push_cart_grant( $cart ); 225 } catch ( \Throwable $e ) { 226 goultra_wc_log( '[GoUltra] warm-list cart grant error: ' . $e->getMessage() ); 227 } 228 } 238 // SECURITY (do NOT grant durable marketing consent from an abandoned cart): 239 // the ghost-capture endpoint is unauthenticated, so its opt-in flag proves the 240 // SENDER wanted the box ticked, NOT that they own the phone. Promoting that to a 241 // permanent "granted" warm-list contact would let anyone forge campaign consent 242 // for a victim's number (every future campaign would reach it). Durable 243 // campaign eligibility is granted ONLY from a VERIFIED signal - a completed 244 // order with marketing consent (maybe_push_order_consent, source woo_checkout) 245 // or an OTP-verified store-manager number. The bounded 3-step recovery below 246 // still runs (the shopper did enter the number + tick consent on this site), but 247 // it never mints a durable subscriber. See the 2026-07-17 security audit. 229 248 } 230 249 … … 366 385 $result = $api->send_abandoned_cart( $payload ); 367 386 368 if ( ! empty( $result['success'] ) ) { 387 // Only record the step as sent when the send actually happened. The backend 388 // can answer HTTP 200 success:true yet NOT have sent a message (e.g. no 389 // APPROVED abandoned-cart template for the WABA yet); in that case it reports 390 // the real outcome in the body. If the body EXPLICITLY says nothing was sent, 391 // do not stamp step{n}_sent_at (so the step can still go out once the template 392 // is approved) and do not inflate the "message sent" count. When there is no 393 // explicit negative signal we keep the prior behaviour (2xx == sent). 394 if ( ! empty( $result['success'] ) && ! self::response_says_not_sent( $result['response'] ?? null ) ) { 369 395 $wpdb->update( 370 396 $table, … … 375 401 $this->log_analytics( 'message_sent', $cart_id, 0, 0 ); 376 402 } 403 } 404 405 /** 406 * True only when the backend response EXPLICITLY indicates no WhatsApp message 407 * was sent (so we must not count it or block a retry). Absent/unknown fields => 408 * false (treat a 2xx as sent, as before) so a newer backend shape never suppresses 409 * a genuine send. 410 * 411 * @param mixed $response Decoded backend body. 412 * @return bool 413 */ 414 private static function response_says_not_sent( $response ) { 415 if ( ! is_array( $response ) ) { 416 return false; 417 } 418 foreach ( array( 'sent', 'meta_sent', 'message_sent' ) as $k ) { 419 if ( array_key_exists( $k, $response ) && false === $response[ $k ] ) { 420 return true; 421 } 422 } 423 if ( isset( $response['_debug'] ) && is_array( $response['_debug'] ) 424 && array_key_exists( 'meta_sent', $response['_debug'] ) && false === $response['_debug']['meta_sent'] ) { 425 return true; 426 } 427 if ( ! empty( $response['skipped'] ) ) { 428 return true; 429 } 430 return false; 377 431 } 378 432 … … 478 532 $table = $wpdb->prefix . 'goultra_abandoned_carts'; 479 533 480 // Find all active (unrecovered) carts for this customer 481 // Match on: normalized phone OR raw phone OR email 534 // Find all active (unrecovered) carts for this customer. 535 // Match on phone OR email. CRITICAL: the capture layer stores the phone in 536 // '+E164' form (GoUltra_Checkout::normalize_phone always prefixes '+'), so we 537 // must match BOTH the plus-less digits AND the '+'-prefixed variant, or a 538 // paying customer would keep getting the "you left items in your cart" 539 // sequence because the phone predicate never matched (email was the only 540 // working key). We build a de-duplicated candidate set covering every format. 541 $phone_candidates = array(); 542 if ( '' !== $normalized_phone ) { 543 $phone_candidates[] = $normalized_phone; // digits only, e.g. 972501234567 544 $phone_candidates[] = '+' . $normalized_phone; // capture format, e.g. +972501234567 545 } 546 $raw = preg_replace( '/[^0-9+]/', '', (string) $phone ); 547 if ( '' !== $raw ) { 548 $phone_candidates[] = $raw; 549 $phone_candidates[] = ltrim( $raw, '+' ); 550 if ( '+' !== substr( $raw, 0, 1 ) ) { 551 $phone_candidates[] = '+' . ltrim( $raw, '+' ); 552 } 553 } 554 $phone_candidates = array_values( array_unique( array_filter( $phone_candidates ) ) ); 555 482 556 $where = array(); 483 557 $values = array(); 484 if ( ! empty( $normalized_phone ) ) { 485 $where[] = 'customer_phone = %s'; 486 $values[] = $normalized_phone; 487 } 488 if ( ! empty( $phone ) && $phone !== $normalized_phone ) { 489 $where[] = 'customer_phone = %s'; 490 $values[] = preg_replace( '/[^0-9+]/', '', $phone ); 558 if ( ! empty( $phone_candidates ) ) { 559 $ph = implode( ',', array_fill( 0, count( $phone_candidates ), '%s' ) ); 560 $where[] = "customer_phone IN ({$ph})"; 561 $values = array_merge( $values, $phone_candidates ); 491 562 } 492 563 if ( ! empty( $email ) ) { … … 537 608 } 538 609 610 // Attribute the recovery to at most ONE analytics event per order, so the 611 // recovered-count / recovered-revenue feed is not double-counted when the 612 // same step-3 message drove BOTH a coupon redemption AND a restore-link click. 613 $recovery_logged = false; 614 539 615 // Check if order used a GU- coupon → track revenue 540 616 $coupons = $order->get_coupon_codes(); … … 553 629 ); 554 630 $this->log_analytics( 'cart_recovered', $cart_id, $order->get_id(), $revenue ); 631 $recovery_logged = true; 555 632 break; 556 633 } 557 634 } 558 635 559 // Also mark recovered via restore_token click (tracked in session/cookie) 636 // Also mark recovered via restore_token click (tracked in session/cookie). 560 637 $token = WC()->session ? WC()->session->get( 'goultra_restore_token' ) : ''; 561 638 if ( $token ) { 562 $matched = $wpdb->get_row( 563 $wpdb->prepare( "SELECT id, cart_total FROM {$table} WHERE restore_token = %s LIMIT 1", $token ) 564 ); 565 if ( $matched ) { 566 $revenue = (float) $order->get_total(); 567 $wpdb->update( $table, array( 'revenue_recovered' => $revenue ), array( 'id' => $matched->id ) ); 568 $this->log_analytics( 'cart_recovered', $matched->id, $order->get_id(), $revenue ); 639 // Only log a SECOND recovery event when the coupon branch did not already 640 // attribute this order (prevents the recovered feed double-counting one 641 // recovery); the token is consumed either way. 642 if ( ! $recovery_logged ) { 643 $matched = $wpdb->get_row( 644 $wpdb->prepare( "SELECT id, cart_total FROM {$table} WHERE restore_token = %s LIMIT 1", $token ) 645 ); 646 if ( $matched ) { 647 $revenue = (float) $order->get_total(); 648 $wpdb->update( $table, array( 'revenue_recovered' => $revenue ), array( 'id' => $matched->id ) ); 649 $this->log_analytics( 'cart_recovered', $matched->id, $order->get_id(), $revenue ); 650 } 569 651 } 570 652 WC()->session->set( 'goultra_restore_token', '' ); -
goultra-for-woocommerce/trunk/includes/class-goultra-activator.php
r3588005 r3617733 119 119 'connected_phone' => '', 120 120 'admin_phone' => '', 121 'enable_order_confirm' => ' yes',122 'enable_shipping_update' => ' yes',123 'enable_admin_notify' => ' yes',124 'enable_abandoned_cart' => ' yes',121 'enable_order_confirm' => 'no', 122 'enable_shipping_update' => 'no', 123 'enable_admin_notify' => 'no', 124 'enable_abandoned_cart' => 'no', 125 125 'abandoned_cart_timeout' => 15, 126 126 // checkout_optin_text is intentionally NOT seeded here: calling __() during -
goultra-for-woocommerce/trunk/includes/class-goultra-api.php
r3599583 r3617733 17 17 /** @var GoUltra_API|null */ 18 18 private static $instance = null; 19 20 /** 21 * Local per-minute send cap PER (endpoint, event) bucket. This is only a local 22 * abuse/runaway guard - the real pacing, tier limits and plan quota all live on 23 * the GoUltra backend. Generous enough that a busy store's order confirmations, 24 * shipping updates and abandoned-cart steps each get their own headroom. 25 */ 26 const RATE_PER_MIN = 200; 19 27 20 28 /** @var string */ … … 137 145 138 146 if ( 200 === $code && ! empty( $body['success'] ) ) { 147 // A good verify proves the key: clear any stale connection warning. 148 update_option( GOULTRA_WC_OPTION_PREFIX . 'auth_fail_count', 0, false ); 149 delete_option( GOULTRA_WC_OPTION_PREFIX . 'connection_warning' ); 139 150 return array( 140 151 'success' => true, … … 143 154 'whatsapp_display_name' => $body['whatsapp_display_name'] ?? '', 144 155 'plan' => $body['plan'] ?? 'free', 156 'profile_picture_url' => $body['profile_picture_url'] ?? '', 145 157 ); 146 158 } … … 171 183 public static function friendly_error( $code ) { 172 184 $code = is_string( $code ) ? $code : ''; 173 if ( 'account_inactive' === $code ) { 174 return __( 'Your GoUltra subscription is not active. Renew it in your GoUltra dashboard to resume sending.', 'goultra-for-woocommerce' ); 185 // A "missing/invalid API key" from a specific feature route (not the connection 186 // itself, which is verified separately) means that route did not accept this 187 // store's per-number key - e.g. campaign sending not yet enabled for this 188 // number on the GoUltra account. Say so plainly instead of the raw backend 189 // string, and do NOT imply the stored key is wrong (order/template features 190 // use the same key and work). 191 $lc = strtolower( $code ); 192 if ( false !== strpos( $lc, 'missing api key' ) || false !== strpos( $lc, 'invalid api key' ) 193 || false !== strpos( $lc, 'plugin service' ) || false !== strpos( $lc, 'woocommerce / shopify plugin' ) ) { 194 return __( 'GoUltra did not accept this store\'s key for this action. Your connection is fine (order messages work), but campaign sending for this number may not be enabled yet on your GoUltra account. Please contact GoUltra support to enable it.', 'goultra-for-woocommerce' ); 195 } 196 if ( false !== strpos( $lc, 'marketing_disabled' ) || false !== strpos( $lc, 'marketing disabled' ) ) { 197 return __( 'Marketing sending is turned off for this WhatsApp number. Enable marketing for this number in your GoUltra dashboard to send campaigns.', 'goultra-for-woocommerce' ); 198 } 199 switch ( $code ) { 200 case 'account_inactive': 201 case 'plan_inactive': 202 case 'no_active_plan': 203 return __( 'Your GoUltra subscription is not active. Renew it in your GoUltra dashboard to resume sending.', 'goultra-for-woocommerce' ); 204 case 'PHONE_NUMBER_REQUIRED': 205 case 'phone_number_id_required': 206 return __( 'Your GoUltra account has more than one WhatsApp number connected, so the sending number must be chosen in the GoUltra dashboard for this store.', 'goultra-for-woocommerce' ); 207 case 'plan_cap': 208 case 'quota_exceeded': 209 case 'messages_monthly': 210 case 'monthly_limit': 211 case 'limit_exceeded': 212 return __( 'Your GoUltra plan message allowance for this cycle has been reached. Upgrade your plan to send more.', 'goultra-for-woocommerce' ); 213 case 'account_blocked': 214 case 'account_suspended': 215 case 'blocked': 216 return __( 'Your GoUltra account is currently on hold, so messages cannot be sent. Please contact GoUltra support to resume.', 'goultra-for-woocommerce' ); 217 case 'too_soon': 218 return __( 'A campaign was just launched. Please wait two minutes before launching another.', 'goultra-for-woocommerce' ); 175 219 } 176 220 return $code; … … 285 329 286 330 if ( is_wp_error( $response ) ) { 287 return $response; 288 } 289 331 return array( 'success' => false, 'error' => $response->get_error_message() ); 332 } 333 334 $code = (int) wp_remote_retrieve_response_code( $response ); 290 335 $body = json_decode( wp_remote_retrieve_body( $response ), true ); 336 if ( ! is_array( $body ) ) { 337 $body = array(); 338 } 339 // If the backend did not signal success, surface a concrete reason so the 340 // merchant (and we) can see WHY a submission failed, not just "failed". 341 if ( empty( $body['success'] ) ) { 342 $reason = ''; 343 foreach ( array( 'error', 'error_code', 'message', 'details' ) as $k ) { 344 if ( ! empty( $body[ $k ] ) && is_string( $body[ $k ] ) ) { 345 $reason = $body[ $k ]; 346 break; 347 } 348 } 349 if ( '' === $reason ) { 350 /* translators: %d: HTTP status code from the GoUltra API. */ 351 $reason = sprintf( __( 'GoUltra returned HTTP %d.', 'goultra-for-woocommerce' ), $code ); 352 } 353 $body['success'] = false; 354 $body['error'] = self::friendly_error( $reason ) ?: $reason; 355 } 291 356 return $body; 357 } 358 359 // ========================================================================= 360 // Admin-number verification (WhatsApp OTP, Mode A). The code is generated AND 361 // verified IN the plugin; GoUltra only DELIVERS it via the Meta AUTH template. 362 // Endpoints /v1/auth/* accept the store's per-number gk_ key (its OTP scope) - 363 // the same endpoints the GoUltra Auth plugin uses successfully. 364 // ========================================================================= 365 366 /** Submit (build) the Meta AUTH-category template for a language. */ 367 public function submit_auth_template( $language ) { 368 return $this->auth_json( $this->request( 'v1/auth/templates/submit', array( 'language' => (string) $language ), 'POST' ) ); 369 } 370 371 /** Live AUTH template statuses (to know if one is APPROVED). */ 372 public function fetch_auth_templates() { 373 return $this->auth_json( $this->request( 'v1/auth/templates', array(), 'GET' ) ); 374 } 375 376 /** Deliver a plugin-generated OTP code to a phone via the AUTH template. */ 377 public function send_auth_code( $phone, $code, $ttl_min, $context, $lang ) { 378 $digits = self::to_e164_digits( (string) $phone ); 379 if ( '' === $digits ) { 380 return array( 'success' => false, 'error' => 'invalid_phone' ); 381 } 382 return $this->auth_json( $this->request( 'v1/auth/send', array( 383 'phone' => '+' . $digits, 384 'code' => (string) $code, 385 'ttl_minutes' => (int) $ttl_min, 386 'context' => (string) $context, 387 'lang' => (string) $lang, 388 'site_url' => home_url(), 389 ), 'POST' ) ); 390 } 391 392 /** Normalize a wp_remote_request result -> { success, code, response, error }. */ 393 private function auth_json( $response ) { 394 if ( is_wp_error( $response ) ) { 395 return array( 'success' => false, 'code' => 0, 'response' => array(), 'error' => $response->get_error_message() ); 396 } 397 $code = (int) wp_remote_retrieve_response_code( $response ); 398 $body = json_decode( wp_remote_retrieve_body( $response ), true ); 399 if ( ! is_array( $body ) ) { 400 $body = array(); 401 } 402 $ok = ( $code >= 200 && $code < 300 ) && empty( $body['error'] ) && ( ! isset( $body['success'] ) || $body['success'] ); 403 $err = ''; 404 if ( ! $ok ) { 405 $err = ! empty( $body['error'] ) ? (string) $body['error'] : ( ! empty( $body['message'] ) ? (string) $body['message'] : 'http_' . $code ); 406 } 407 return array( 'success' => $ok, 'code' => $code, 'response' => $body, 'error' => $err ); 292 408 } 293 409 … … 306 422 * @return array { success: bool, code?: int, response?: array, error?: string } 307 423 */ 424 /** 425 * Update a subscriber's display name in the GoUltra contacts registry. 426 * Name only - phone/email are not changed. Backend endpoint: 427 * POST /api/v1/wc/subscribers/update { site_url, phone, name }. 428 * 429 * @param string $phone E.164 (with or without +). 430 * @param string $name 431 * @return array { success, code, response } 432 */ 433 public function update_subscriber( $phone, $name ) { 434 $digits = self::to_e164_digits( (string) $phone ); 435 if ( '' === $digits ) { 436 return array( 'success' => false, 'error' => 'invalid phone' ); 437 } 438 $response = $this->request( 'v1/wc/subscribers/update', array( 439 'site_url' => home_url(), 440 'phone' => '+' . $digits, 441 'name' => (string) $name, 442 ), 'POST' ); 443 if ( is_wp_error( $response ) ) { 444 return array( 'success' => false, 'error' => $response->get_error_message() ); 445 } 446 $code = (int) wp_remote_retrieve_response_code( $response ); 447 $body = json_decode( wp_remote_retrieve_body( $response ), true ); 448 return array( 449 'success' => ( $code >= 200 && $code < 300 ), 450 'code' => $code, 451 'response' => is_array( $body ) ? $body : array(), 452 ); 453 } 454 308 455 public function push_consent( $contacts ) { 309 456 if ( empty( $contacts ) || ! is_array( $contacts ) ) { … … 450 597 } 451 598 599 // ========================================================================= 600 // Marketing campaigns - account-level backend contract (worker-confirmed 601 // 2026-07-12, prod v2.8.931): the account api_key is authorized on 602 // /api/campaigns/*, /api/meta-templates and /v1/messages. Opt-outs are 603 // filtered twice server-side (submit-time + the per-message IRON DOME). 604 // ========================================================================= 605 606 /** 607 * Small wrapper: perform a request and return a uniform 608 * { success, code, response } array (response = decoded JSON body). 609 * 610 * @param string $endpoint Relative endpoint (under the /api base). 611 * @param array $body 612 * @param string $method 613 * @return array 614 */ 615 private function request_json( $endpoint, $body = array(), $method = 'POST' ) { 616 $response = $this->request( $endpoint, $body, $method ); 617 if ( is_wp_error( $response ) ) { 618 return array( 'success' => false, 'code' => 0, 'response' => null, 'error' => $response->get_error_message() ); 619 } 620 $code = (int) wp_remote_retrieve_response_code( $response ); 621 return array( 622 'success' => ( $code >= 200 && $code < 300 ), 623 'code' => $code, 624 'response' => json_decode( wp_remote_retrieve_body( $response ), true ), 625 ); 626 } 627 628 /** 629 * Request against the backend ORIGIN (https://go.goultra.ai) rather than the 630 * /api base - the /v1/messages send endpoint lives at the origin root. 631 * 632 * @param string $path Absolute path from the origin, e.g. 'v1/messages'. 633 * @param array $body 634 * @param string $method 635 * @return array { success, code, response } 636 */ 637 private function request_origin_json( $path, $body = array(), $method = 'POST' ) { 638 $origin = preg_replace( '#/api/?$#', '', rtrim( GOULTRA_WC_API_BASE, '/' ) ); 639 $url = $origin . '/' . ltrim( $path, '/' ); 640 $args = array( 641 'method' => $method, 642 'headers' => array( 643 'Content-Type' => 'application/json', 644 'Authorization' => 'Bearer ' . $this->api_key, 645 'X-Plugin' => 'goultra-wc/' . GOULTRA_WC_VERSION, 646 'X-Site-URL' => home_url(), 647 ), 648 'timeout' => 20, 649 ); 650 if ( 'GET' !== $method && ! empty( $body ) ) { 651 $args['body'] = wp_json_encode( $body ); 652 } 653 $response = wp_remote_request( $url, $args ); 654 if ( is_wp_error( $response ) ) { 655 return array( 'success' => false, 'code' => 0, 'response' => null, 'error' => $response->get_error_message() ); 656 } 657 $code = (int) wp_remote_retrieve_response_code( $response ); 658 return array( 659 'success' => ( $code >= 200 && $code < 300 ), 660 'code' => $code, 661 'response' => json_decode( wp_remote_retrieve_body( $response ), true ), 662 ); 663 } 664 665 /** All templates (rich shape: body, buttons, param_count, category, status). */ 666 public function fetch_meta_templates() { 667 return $this->request_json( 'meta-templates', array(), 'GET' ); 668 } 669 670 /** Create a template via the rich creator (used for MARKETING promos). */ 671 public function create_meta_template( $payload ) { 672 return $this->request_json( 'meta-templates', $payload, 'POST' ); 673 } 674 675 /** One-off test message (/v1/messages at the origin). */ 676 public function send_test_message( $payload ) { 677 return $this->request_origin_json( 'v1/messages', $payload, 'POST' ); 678 } 679 680 /** 681 * Campaign request with a number-aware first hop. 682 * 683 * The store connects with its PER-NUMBER key (gk_live_...). The legacy campaign 684 * routes live under /api/campaigns/* which authenticate with the ACCOUNT key only 685 * (validateApiKey) and reject a per-number key with "Missing API key" - the same 686 * gap that affected template building until the backend made /api/v1/wc/* accept 687 * per-number keys. So we try the number-aware /api/v1/wc/campaigns/* route FIRST 688 * (that surface already accepts gk_ for verify / subscribers / templates); if the 689 * backend does not expose it yet we get a 404 and fall back to the legacy 690 * /api/campaigns/* route. A 404 means the request was NOT processed, so this can 691 * never double-send. Once the backend accepts the per-number key on either route, 692 * campaigns work with no further plugin change. 693 * 694 * @param string $suffix e.g. 'queue-send', '', '{id}', '{id}/pause' 695 * @param array $body 696 * @param string $method 697 * @return array { success, code, response } 698 */ 699 private function campaign_request( $suffix, $body, $method ) { 700 $suffix = ltrim( (string) $suffix, '/' ); 701 $v1 = 'v1/wc/campaigns' . ( '' !== $suffix ? '/' . $suffix : '' ); 702 $res = $this->request_json( $v1, $body, $method ); 703 if ( 404 === (int) ( $res['code'] ?? 0 ) ) { 704 $legacy = 'campaigns' . ( '' !== $suffix ? '/' . $suffix : '' ); 705 $res = $this->request_json( $legacy, $body, $method ); 706 } 707 return $res; 708 } 709 710 /** Launch an immediate campaign. */ 711 public function queue_campaign( $payload ) { 712 return $this->campaign_request( 'queue-send', $payload, 'POST' ); 713 } 714 715 /** Create a scheduled campaign (the backend cron fires it). */ 716 public function create_scheduled_campaign( $payload ) { 717 return $this->campaign_request( '', $payload, 'POST' ); 718 } 719 720 /** Campaign status + progress. */ 721 public function get_campaign( $campaign_id ) { 722 return $this->campaign_request( rawurlencode( (string) $campaign_id ), array(), 'GET' ); 723 } 724 725 /** Pause a running campaign. */ 726 public function pause_campaign( $campaign_id ) { 727 return $this->campaign_request( rawurlencode( (string) $campaign_id ) . '/pause', array(), 'POST' ); 728 } 729 730 /** Cancel a scheduled/running campaign. */ 731 public function cancel_campaign( $campaign_id ) { 732 return $this->campaign_request( rawurlencode( (string) $campaign_id ), array(), 'DELETE' ); 733 } 734 452 735 /** 453 736 * Generic event sender. … … 459 742 */ 460 743 private function send_event( $event, $payload, $endpoint ) { 461 // Rate limiting - max 60 requests per minute 462 $rate_key = 'goultra_rate_' . md5( $endpoint ); 744 // Never fire an outbound send while the store is not connected. This is the 745 // single choke point for order/shipping/abandoned-cart/policy sends, so a 746 // disconnected store (or one whose key was cleared) can never POST an empty 747 // "Authorization: Bearer " to the backend carrying the webhook secret. 748 $this->refresh_api_key(); 749 if ( '' === (string) $this->api_key ) { 750 return array( 'success' => false, 'error' => 'not_connected' ); 751 } 752 753 // Rate limiting (local abuse guard; real pacing/quota are server-side). 754 // FIXED per-minute window keyed by (endpoint, event, minute): a fresh key 755 // each minute means the cap can never "slide" (the old code renewed the TTL 756 // on every hit, so under sustained load the window never reset). Separate 757 // buckets per event type so marketing bursts can't starve order/shipping. 758 $window = (int) floor( time() / MINUTE_IN_SECONDS ); 759 $rate_key = 'goultra_rate_' . md5( $endpoint . '|' . (string) ( $event ?? '' ) ) . '_' . $window; 463 760 $count = (int) get_transient( $rate_key ); 464 if ( $count >= 60) {761 if ( $count >= self::RATE_PER_MIN ) { 465 762 $this->log_webhook( $event ?? 'rate_limited', $endpoint, $payload, 429, 'Rate limited' ); 466 763 return array( 'success' => false, 'error' => 'Rate limited' ); 467 764 } 468 set_transient( $rate_key, $count + 1, MINUTE_IN_SECONDS );765 set_transient( $rate_key, $count + 1, 2 * MINUTE_IN_SECONDS ); 469 766 470 767 $body = array( … … 493 790 494 791 $decoded = json_decode( wp_remote_retrieve_body( $response ), true ); 792 // Track the credential health so a revoked/rotated key surfaces as a visible 793 // "connection problem" instead of silently failing every automated send. 794 $this->note_auth_health( (int) $resp_code, is_array( $decoded ) ? $decoded : array() ); 495 795 return array( 496 796 'success' => ( $resp_code >= 200 && $resp_code < 300 ), … … 498 798 'response' => $decoded, 499 799 ); 800 } 801 802 /** 803 * Watch outbound responses for authentication failures. A store owner can 804 * regenerate their per-number key in the GoUltra dashboard at any time; when 805 * that happens every automated send starts failing with 401/invalid-key and 806 * nothing here ever flips 'connected'. To avoid silently dropping every message 807 * while the UI shows a green "Connected", we count consecutive auth failures and, 808 * after a few in a row, raise a visible warning flag (surfaced as an admin notice). 809 * We NEVER auto-clear the key/connection - a transient backend outage must not 810 * disconnect the store; only the owner disconnects. 811 * 812 * @param int $code HTTP status. 813 * @param array $decoded Decoded JSON body. 814 */ 815 private function note_auth_health( $code, $decoded ) { 816 $err = strtolower( (string) ( $decoded['error'] ?? $decoded['error_code'] ?? '' ) ); 817 $is_auth_fail = in_array( $code, array( 401, 403 ), true ) 818 || false !== strpos( $err, 'missing api key' ) 819 || false !== strpos( $err, 'invalid api key' ) 820 || false !== strpos( $err, 'invalid_api_key' ) 821 || false !== strpos( $err, 'unauthorized' ); 822 823 if ( $code >= 200 && $code < 300 ) { 824 // A clean send: the key is good again. Clear any prior warning. 825 if ( get_option( GOULTRA_WC_OPTION_PREFIX . 'auth_fail_count', 0 ) || get_option( GOULTRA_WC_OPTION_PREFIX . 'connection_warning', '' ) ) { 826 update_option( GOULTRA_WC_OPTION_PREFIX . 'auth_fail_count', 0, false ); 827 delete_option( GOULTRA_WC_OPTION_PREFIX . 'connection_warning' ); 828 } 829 return; 830 } 831 832 if ( ! $is_auth_fail ) { 833 return; // a non-auth failure (network/backend) must not raise the key warning 834 } 835 836 $n = (int) get_option( GOULTRA_WC_OPTION_PREFIX . 'auth_fail_count', 0 ) + 1; 837 update_option( GOULTRA_WC_OPTION_PREFIX . 'auth_fail_count', $n, false ); 838 if ( $n >= 3 ) { 839 update_option( GOULTRA_WC_OPTION_PREFIX . 'connection_warning', time(), false ); 840 } 500 841 } 501 842 … … 546 887 $table = $wpdb->prefix . 'goultra_webhook_logs'; 547 888 548 // Sanitize sensitive data from logs 889 // Sanitize sensitive data from logs. 549 890 $sanitized_body = $request_body; 891 // Never persist the inbound webhook secret to the log table: it authenticates 892 // the backend->store /webhook and /sync calls, so a log export / partial DB 893 // backup / log-viewer role must not hand it over. It still goes to the backend 894 // in the real request; only this local audit copy is redacted. 895 if ( isset( $sanitized_body['wc_webhook_secret'] ) ) { 896 $sanitized_body['wc_webhook_secret'] = '[redacted]'; 897 } 550 898 if ( isset( $sanitized_body['payload']['customer_phone'] ) ) { 551 899 $phone = $sanitized_body['payload']['customer_phone']; -
goultra-for-woocommerce/trunk/includes/class-goultra-checkout.php
r3588005 r3617733 190 190 } 191 191 192 if ( 'yes' !== get_option( GOULTRA_WC_OPTION_PREFIX . 'enable_abandoned_cart', ' yes' ) ) {192 if ( 'yes' !== get_option( GOULTRA_WC_OPTION_PREFIX . 'enable_abandoned_cart', 'no' ) ) { 193 193 return; 194 194 } … … 328 328 $wpdb->update( $table, $data, array( 'id' => $existing->id ) ); 329 329 } else { 330 // Anti-flood: cap how many active (unrecovered) rows can target ONE phone 331 // across different sessions. Without this a bot rotating WooCommerce 332 // sessions could seed many rows for a victim's number (all opted-in) and 333 // aim repeated marketing messages at it. The per-session upsert above 334 // already collapses same-session captures; this bounds cross-session ones. 335 $active_for_phone = (int) $wpdb->get_var( 336 $wpdb->prepare( 337 "SELECT COUNT(*) FROM {$table} WHERE customer_phone = %s AND recovered = 0", 338 $phone 339 ) 340 ); 341 if ( $active_for_phone >= 3 ) { 342 // Silently succeed without storing a new row (do not leak the cap). 343 wp_send_json_success( array( 'stored' => false ) ); 344 return; 345 } 330 346 $wpdb->insert( $table, $data ); 331 347 } -
goultra-for-woocommerce/trunk/includes/class-goultra-orders.php
r3599583 r3617733 8 8 * ⚡ PERFORMANCE: every outbound HTTP call to GoUltra is dispatched to an 9 9 * Action Scheduler ASYNC job (`goultra_async_send`) instead of running inline on 10 * the checkout / order-status request. This guarantees the customer's checkout is10 * the checkout / order-status request. This ensures the customer's checkout is 11 11 * NEVER blocked or slowed by the GoUltra API (even if it is slow or down). 12 12 * Shipping updates are handled separately by class-goultra-shipping.php. … … 101 101 } 102 102 // Backward compat: no mapping → use legacy enable_admin_notify (once per order) 103 elseif ( empty( $status_map ) && 'yes' === get_option( GOULTRA_WC_OPTION_PREFIX . 'enable_admin_notify', ' yes' ) ) {103 elseif ( empty( $status_map ) && 'yes' === get_option( GOULTRA_WC_OPTION_PREFIX . 'enable_admin_notify', 'no' ) ) { 104 104 if ( ! $order->get_meta( '_goultra_admin_notify_sent' ) ) { 105 105 $order->update_meta_data( '_goultra_admin_notify_sent', gmdate( 'Y-m-d H:i:s' ) ); … … 116 116 // Backward compat: trigger by legacy order_confirm_trigger + enable_order_confirm 117 117 $trigger = get_option( GOULTRA_WC_OPTION_PREFIX . 'order_confirm_trigger', 'processing' ); 118 if ( $trigger === $current_status && 'yes' === get_option( GOULTRA_WC_OPTION_PREFIX . 'enable_order_confirm', ' yes' ) ) {118 if ( $trigger === $current_status && 'yes' === get_option( GOULTRA_WC_OPTION_PREFIX . 'enable_order_confirm', 'no' ) ) { 119 119 $should_send_customer = true; 120 120 } -
goultra-for-woocommerce/trunk/includes/class-goultra-rest-api.php
r3588005 r3617733 125 125 if ( empty( $status ) ) { 126 126 return new WP_REST_Response( array( 'success' => false, 'error' => 'status required' ), 400 ); 127 } 128 129 // ── Replay guard ────────────────────────────────────────────────────── 130 // A status webhook can be retried by the backend (or replayed by anyone who 131 // holds the secret). De-dupe on the WhatsApp message id + status so we never 132 // add the same order note twice or double-log. Keyed for a day; only applied 133 // when we have a message id to key on. 134 if ( '' !== $wa_msg_id ) { 135 $seen_key = 'goultra_dlv_' . md5( $wa_msg_id . '|' . $status ); 136 if ( get_transient( $seen_key ) ) { 137 return new WP_REST_Response( array( 'success' => true, 'duplicate' => true ), 200 ); 138 } 139 set_transient( $seen_key, 1, DAY_IN_SECONDS ); 127 140 } 128 141 … … 153 166 } 154 167 155 // ── Log to webhook_logs table ─────────────────────────────────────────168 // ── Log to webhook_logs table (phone masked, table bounded) ─────────── 156 169 global $wpdb; 170 $log_table = $wpdb->prefix . 'goultra_webhook_logs'; 171 $masked_body = is_array( $body ) ? $body : array(); 172 if ( ! empty( $masked_body['phone'] ) ) { 173 $p = (string) $masked_body['phone']; 174 $masked_body['phone'] = ( strlen( $p ) > 6 ) 175 ? substr( $p, 0, 4 ) . '****' . substr( $p, -2 ) 176 : '****'; 177 } 157 178 $wpdb->insert( 158 $ wpdb->prefix . 'goultra_webhook_logs',179 $log_table, 159 180 array( 160 181 'event_type' => 'delivery_status_' . $status, 161 182 'endpoint' => 'goultra/v1/webhook', 162 'request_body' => wp_json_encode( $ body ),183 'request_body' => wp_json_encode( $masked_body ), 163 184 'response_code' => 200, 164 185 'response_body' => 'ok', 165 186 ) 166 187 ); 188 // Keep the log table bounded (inbound rows previously bypassed the outbound 189 // trim, so a busy store's delivery receipts could grow it unchecked). 190 $log_count = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$log_table}" ); 191 if ( $log_count > 500 ) { 192 $keep_id = $wpdb->get_var( $wpdb->prepare( "SELECT id FROM {$log_table} ORDER BY id DESC LIMIT 1 OFFSET %d", 499 ) ); 193 if ( $keep_id ) { 194 $wpdb->query( $wpdb->prepare( "DELETE FROM {$log_table} WHERE id < %d", $keep_id ) ); 195 } 196 } 167 197 168 198 // ── Analytics: track read/delivered for abandoned cart messages ─────── -
goultra-for-woocommerce/trunk/includes/class-goultra-settings.php
r3603449 r3617733 25 25 add_action( 'admin_menu', array( $this, 'add_admin_menu' ) ); 26 26 add_action( 'wp_ajax_goultra_debug_sync', array( $this, 'ajax_debug_sync' ) ); 27 add_action( 'wp_ajax_goultra_test_send', array( $this, 'ajax_test_send' ) );28 27 add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_assets' ) ); 29 28 add_action( 'wp_ajax_goultra_verify_api_key', array( $this, 'ajax_verify_api_key' ) ); … … 34 33 // Show setup wizard on first install 35 34 add_action( 'admin_init', array( $this, 'maybe_redirect_to_setup' ) ); 35 36 // Surface a visible warning when the stored API key stops authenticating 37 // (e.g. the owner regenerated their per-number key in the GoUltra dashboard), 38 // so a "green Connected" store never silently drops every automated message. 39 add_action( 'admin_notices', array( $this, 'maybe_connection_warning' ) ); 40 } 41 42 /** 43 * Admin notice shown on GoUltra screens when recent automated sends have been 44 * rejected for authentication. We do NOT auto-disconnect (a transient backend 45 * outage must not clear the connection); we only tell the owner to reconnect. 46 */ 47 public function maybe_connection_warning() { 48 if ( ! current_user_can( 'manage_woocommerce' ) ) { 49 return; 50 } 51 if ( ! get_option( GOULTRA_WC_OPTION_PREFIX . 'connected', false ) ) { 52 return; 53 } 54 if ( ! get_option( GOULTRA_WC_OPTION_PREFIX . 'connection_warning', '' ) ) { 55 return; 56 } 57 $url = admin_url( 'admin.php?page=goultra-settings' ); 58 echo '<div class="notice notice-error"><p><strong>' 59 . esc_html__( 'GoUltra for WooCommerce: connection problem.', 'goultra-for-woocommerce' ) . '</strong> ' 60 . esc_html__( 'Recent WhatsApp messages were rejected because the API key is no longer valid (it may have been regenerated or the account changed). Messages are not being delivered.', 'goultra-for-woocommerce' ) 61 . ' <a href="' . esc_url( $url ) . '">' 62 . esc_html__( 'Reconnect your store', 'goultra-for-woocommerce' ) . '</a></p></div>'; 36 63 } 37 64 … … 94 121 ); 95 122 123 // Campaigns submenu 124 add_submenu_page( 125 'goultra-settings', 126 __( 'Campaigns', 'goultra-for-woocommerce' ), 127 __( 'Campaigns', 'goultra-for-woocommerce' ), 128 'manage_woocommerce', 129 'goultra-campaigns', 130 array( $this, 'render_campaigns_page' ) 131 ); 132 96 133 // Logs submenu 97 134 add_submenu_page( … … 115 152 'goultra_page_goultra-templates', 116 153 'goultra_page_goultra-subscribers', 154 'goultra_page_goultra-campaigns', 117 155 'goultra_page_goultra-logs', 118 156 ); … … 173 211 'none' => __( 'No subscribers yet. They appear here once shoppers opt in to WhatsApp marketing at checkout.', 'goultra-for-woocommerce' ), 174 212 'loading' => __( 'Loading...', 'goultra-for-woocommerce' ), 213 'phoneRequired' => __( 'Phone number is required.', 'goultra-for-woocommerce' ), 214 'adding' => __( 'Adding...', 'goultra-for-woocommerce' ), 215 'added' => __( 'Added to the list.', 'goultra-for-woocommerce' ), 216 'addError' => __( 'Could not add the contact. Please try again.', 'goultra-for-woocommerce' ), 217 'editName' => __( 'Edit name', 'goultra-for-woocommerce' ), 218 'save' => __( 'Save', 'goultra-for-woocommerce' ), 219 'cancel' => __( 'Cancel', 'goultra-for-woocommerce' ), 175 220 'error' => __( 'Could not load subscribers. Please try again.', 'goultra-for-woocommerce' ), 176 221 'subscribed' => __( 'Subscribed', 'goultra-for-woocommerce' ), … … 191 236 ) ); 192 237 } 238 239 // Campaigns screen: its own script + the media library + the preset data. 240 if ( 'goultra_page_goultra-campaigns' === $hook ) { 241 wp_enqueue_media(); 242 wp_enqueue_script( 243 'goultra-campaigns', 244 GOULTRA_WC_PLUGIN_URL . 'assets/js/campaigns.js', 245 array( 'jquery' ), 246 GOULTRA_WC_VERSION, 247 true 248 ); 249 wp_localize_script( 'goultra-campaigns', 'guCamp', array( 250 'ajaxUrl' => admin_url( 'admin-ajax.php' ), 251 'nonce' => wp_create_nonce( 'goultra_admin_nonce' ), 252 'i18n' => array( 253 'loading' => __( 'Loading...', 'goultra-for-woocommerce' ), 254 'error' => __( 'Something went wrong. Please try again.', 'goultra-for-woocommerce' ), 255 'chooseTpl' => __( 'Choose a template', 'goultra-for-woocommerce' ), 256 'choosePreview' => __( 'Choose a template to see the preview.', 'goultra-for-woocommerce' ), 257 'pending' => __( 'Awaiting Meta approval:', 'goultra-for-woocommerce' ), 258 'varName' => __( 'Customer first name', 'goultra-for-woocommerce' ), 259 'varFull' => __( 'Customer full name', 'goultra-for-woocommerce' ), 260 'varFixed' => __( 'Fixed value', 'goultra-for-woocommerce' ), 261 'headerImage' => __( 'Header image', 'goultra-for-woocommerce' ), 262 'jpegOnly' => __( 'JPEG or PNG images only.', 'goultra-for-woocommerce' ), 263 'noProducts' => __( 'No products with a usable image were found.', 'goultra-for-woocommerce' ), 264 'noSubs' => __( 'No subscribers yet.', 'goultra-for-woocommerce' ), 265 'sending' => __( 'Sending...', 'goultra-for-woocommerce' ), 266 'fillVars' => __( 'Please fill in all template variables.', 'goultra-for-woocommerce' ), 267 'headerRequired' => __( 'This template needs a header image. Choose one from the media library, a product, or paste an https image link.', 'goultra-for-woocommerce' ), 268 'noRecipients' => __( 'No recipients selected. Choose contacts from the list.', 'goultra-for-woocommerce' ), 269 'noName' => __( 'No name', 'goultra-for-woocommerce' ), 270 /* translators: %d: number of subscribers. */ 271 'allSubs' => __( 'All subscribers (%d)', 'goultra-for-woocommerce' ), 272 /* translators: %d: number of selected contacts. */ 273 'nSelected' => __( '%d selected', 'goultra-for-woocommerce' ), 274 /* translators: %d: number of recipients. */ 275 'reach' => __( '%d recipients will receive this campaign.', 'goultra-for-woocommerce' ), 276 /* translators: %d: number of recipients. */ 277 'confirmSend' => __( 'Send this campaign to %d recipients now?', 'goultra-for-woocommerce' ), 278 /* translators: %d: number of recipients. */ 279 'confirmSched' => __( 'Schedule this campaign to %d recipients?', 'goultra-for-woocommerce' ), 280 'launchBtn' => __( 'Launch campaign', 'goultra-for-woocommerce' ), 281 'scheduleBtn' => __( 'Schedule campaign', 'goultra-for-woocommerce' ), 282 /* translators: %1$s: date, %2$s: time. */ 283 'schedNote' => __( 'Will be sent on %1$s at %2$s (your store time).', 'goultra-for-woocommerce' ), 284 'launchedOk' => __( 'Campaign launched. Messages are on their way.', 'goultra-for-woocommerce' ), 285 'scheduledOk' => __( 'Campaign scheduled.', 'goultra-for-woocommerce' ), 286 'noCampaigns' => __( 'No campaigns yet.', 'goultra-for-woocommerce' ), 287 'pause' => __( 'Pause', 'goultra-for-woocommerce' ), 288 'cancel' => __( 'Cancel', 'goultra-for-woocommerce' ), 289 'confirmPause' => __( 'Pause this campaign?', 'goultra-for-woocommerce' ), 290 'confirmCancel' => __( 'Cancel this scheduled campaign?', 'goultra-for-woocommerce' ), 291 'st_processing' => __( 'Processing', 'goultra-for-woocommerce' ), 292 'st_scheduled' => __( 'Scheduled', 'goultra-for-woocommerce' ), 293 'st_completed' => __( 'Completed', 'goultra-for-woocommerce' ), 294 'st_failed' => __( 'Failed', 'goultra-for-woocommerce' ), 295 'st_cancelled' => __( 'Cancelled', 'goultra-for-woocommerce' ), 296 'st_paused' => __( 'Paused', 'goultra-for-woocommerce' ), 297 'st_unknown' => __( 'Unconfirmed - verify before resending', 'goultra-for-woocommerce' ), 298 ), 299 ) ); 300 } 193 301 } 194 302 … … 217 325 update_option( GOULTRA_WC_OPTION_PREFIX . 'business_name', $result['business_name'] ?? '' ); 218 326 update_option( GOULTRA_WC_OPTION_PREFIX . 'whatsapp_display_name', $result['whatsapp_display_name'] ?? '' ); 327 // WhatsApp profile picture for the template/campaign preview (public, 328 // shown to every recipient). The backend signs this URL and it expires 329 // after ~15 minutes, so we store it WITH a timestamp and refresh it 330 // (throttled) via wa_avatar_url(); the preview falls back to the site 331 // icon when it is empty or expired. 332 $fresh_avatar = ! empty( $result['profile_picture_url'] ) ? esc_url_raw( $result['profile_picture_url'] ) : ''; 333 update_option( GOULTRA_WC_OPTION_PREFIX . 'wa_avatar', $fresh_avatar, false ); 334 update_option( GOULTRA_WC_OPTION_PREFIX . 'wa_avatar_ts', time(), false ); 219 335 $api->refresh_api_key(); 220 336 … … 233 349 } 234 350 351 // Kick off building the WhatsApp AUTH template (for admin-number 352 // verification) so it is ready/approved by the time the owner verifies. 353 try { 354 if ( class_exists( 'GoUltra_Admin_Verify' ) ) { 355 GoUltra_Admin_Verify::instance()->ensure_auth_template(); 356 } 357 } catch ( \Throwable $e ) { 358 goultra_wc_log( '[GoUltra] connect auth-template error: ' . $e->getMessage() ); 359 } 360 235 361 wp_send_json_success( $result ); 236 362 } else { 237 363 wp_send_json_error( $result['error'] ); 238 364 } 365 } 366 367 /** 368 * WhatsApp business profile picture URL for the template / campaign preview. 369 * 370 * The backend serves a signed URL that expires after ~15 minutes, so we do 371 * not store it permanently: we cache it with a timestamp and re-fetch it 372 * (throttled to at most once every 10 minutes, via verify) when it is empty 373 * or stale. When no WhatsApp picture is available it falls back to the site 374 * icon, so the preview always shows something. Safe to call on page render. 375 * 376 * @return string 377 */ 378 public static function wa_avatar_url() { 379 $url = (string) get_option( GOULTRA_WC_OPTION_PREFIX . 'wa_avatar', '' ); 380 $ts = (int) get_option( GOULTRA_WC_OPTION_PREFIX . 'wa_avatar_ts', 0 ); 381 $connected = get_option( GOULTRA_WC_OPTION_PREFIX . 'connected' ); 382 $stale = ( '' === $url ) || ( time() - $ts > 10 * MINUTE_IN_SECONDS ); 383 384 if ( $connected && $stale && ! get_transient( 'goultra_wa_avatar_lock' ) ) { 385 // Throttle: one refresh attempt per 10 minutes regardless of outcome. 386 set_transient( 'goultra_wa_avatar_lock', 1, 10 * MINUTE_IN_SECONDS ); 387 try { 388 $key = (string) get_option( GOULTRA_WC_OPTION_PREFIX . 'api_key', '' ); 389 if ( '' !== $key ) { 390 $api = GoUltra_API::instance(); 391 $api->refresh_api_key(); 392 $res = $api->verify_connection( $key ); 393 if ( ! empty( $res['success'] ) ) { 394 $url = ! empty( $res['profile_picture_url'] ) ? esc_url_raw( $res['profile_picture_url'] ) : ''; 395 update_option( GOULTRA_WC_OPTION_PREFIX . 'wa_avatar', $url, false ); 396 update_option( GOULTRA_WC_OPTION_PREFIX . 'wa_avatar_ts', time(), false ); 397 } 398 } 399 } catch ( \Throwable $e ) { 400 goultra_wc_log( '[GoUltra] wa_avatar refresh error: ' . $e->getMessage() ); 401 } 402 } 403 404 if ( '' === $url ) { 405 $url = (string) get_site_icon_url( 96 ); 406 } 407 return $url; 239 408 } 240 409 … … 284 453 'ac_discount_type' => 'sanitize_text_field', 285 454 'ac_discount_percent' => 'absint', 455 // Free WhatsApp chat button (front-end, no account needed). 456 'chat_button_enabled' => 'sanitize_text_field', 457 'chat_button_phone' => 'sanitize_text_field', 458 'chat_button_message' => 'sanitize_text_field', 459 'chat_button_label' => 'sanitize_text_field', 460 'chat_button_position' => 'sanitize_text_field', 461 'chat_button_visibility' => 'sanitize_text_field', 462 'chat_button_product' => 'sanitize_text_field', 286 463 ); 287 464 … … 330 507 } 331 508 509 // Remove the verified store-manager number from the marketing list FIRST, while 510 // the API key is still valid - the unsubscribe call needs to authenticate, so it 511 // must run BEFORE we wipe the key below (otherwise it would fire with an empty 512 // Bearer and the contact would be left on the list). 513 if ( class_exists( 'GoUltra_Admin_Verify' ) ) { 514 try { 515 GoUltra_Admin_Verify::instance()->clear_verified(); 516 } catch ( \Throwable $e ) { 517 goultra_wc_log( '[GoUltra] disconnect clear_verified error: ' . $e->getMessage() ); 518 } 519 } 520 332 521 update_option( GOULTRA_WC_OPTION_PREFIX . 'api_key', '' ); 333 522 update_option( GOULTRA_WC_OPTION_PREFIX . 'connected', false ); … … 352 541 delete_transient( 'goultra_wc_remote_templates' ); 353 542 543 // Clear the credential-health warning so a later reconnect starts clean. 544 update_option( GOULTRA_WC_OPTION_PREFIX . 'auth_fail_count', 0, false ); 545 delete_option( GOULTRA_WC_OPTION_PREFIX . 'connection_warning' ); 546 547 // Clear the verified store-manager number and drop it from the marketing list 548 // (it was only added for the owner's own campaign tests while connected). 549 if ( class_exists( 'GoUltra_Admin_Verify' ) ) { 550 try { 551 GoUltra_Admin_Verify::instance()->clear_verified(); 552 } catch ( \Throwable $e ) { 553 goultra_wc_log( '[GoUltra] disconnect clear_verified error: ' . $e->getMessage() ); 554 } 555 } 556 354 557 wp_send_json_success(); 355 558 } … … 414 617 415 618 /** 619 * Render the Campaigns page. 620 */ 621 public function render_campaigns_page() { 622 if ( ! current_user_can( 'manage_woocommerce' ) ) { return; } 623 require_once GOULTRA_WC_PLUGIN_DIR . 'templates/admin/campaigns-page.php'; 624 } 625 626 /** 416 627 * AJAX: Debug sync - returns raw data from GoUltra and updates WP DB. 417 628 */ 418 /**419 * AJAX: Send a test WhatsApp message to a phone number via GoUltra API.420 */421 public function ajax_test_send() {422 check_ajax_referer( 'goultra_admin_nonce', 'nonce' );423 if ( ! current_user_can( 'manage_woocommerce' ) ) {424 wp_send_json_error( 'Permission denied' );425 }426 $phone = sanitize_text_field( wp_unslash( $_POST['phone'] ?? '' ) );427 $order_id = intval( wp_unslash( $_POST['order_id'] ?? 0 ) );428 if ( empty( $phone ) ) {429 wp_send_json_error( 'Phone required' );430 }431 432 // Normalize phone433 $raw = preg_replace( '/[^0-9+]/', '', $phone );434 if ( substr( $raw, 0, 1 ) === '+' ) {435 $raw = ltrim( $raw, '+' );436 } elseif ( substr( $raw, 0, 2 ) === '00' ) {437 $raw = substr( $raw, 2 );438 } elseif ( substr( $raw, 0, 1 ) === '0' ) {439 $raw = '972' . substr( $raw, 1 ); // default IL440 }441 442 if ( $order_id > 0 ) {443 $order = wc_get_order( $order_id );444 if ( $order ) {445 // Clear dedup so it can send again446 $order->delete_meta_data( '_goultra_order_confirm_sent' );447 $order->save();448 // Force send449 GoUltra_Orders::instance();450 require_once GOULTRA_WC_PLUGIN_DIR . 'includes/class-goultra-orders.php';451 $orders = new ReflectionClass( 'GoUltra_Orders' );452 $method = $orders->getMethod( 'send_order_confirmation' );453 $method->setAccessible( true );454 $method->invoke( GoUltra_Orders::instance(), $order );455 wp_send_json_success( array( 'message' => 'Sent via order #' . $order_id ) );456 return;457 }458 }459 460 // Direct API call461 $api = GoUltra_API::instance();462 $api->refresh_api_key();463 $response = $api->send_order_confirmation( array(464 'phone' => $raw,465 'first_name' => 'Test',466 'last_name' => 'User',467 'order_id' => 99999,468 'order_number' => '99999',469 'order_total' => '100.00',470 'currency' => 'ILS',471 'items_summary' => 'Test Product x1',472 'order_url' => home_url(),473 'template_lang' => get_option( GOULTRA_WC_OPTION_PREFIX . 'default_template_lang', 'ar' ),474 ) );475 wp_send_json_success( array( 'response' => $response, 'phone_sent' => $raw ) );476 }477 478 629 public function ajax_debug_sync() { 479 630 check_ajax_referer( 'goultra_admin_nonce', 'nonce' ); -
goultra-for-woocommerce/trunk/includes/class-goultra-shipping.php
r3599583 r3617733 44 44 } 45 45 46 if ( 'yes' !== get_option( GOULTRA_WC_OPTION_PREFIX . 'enable_shipping_update', ' yes' ) ) {46 if ( 'yes' !== get_option( GOULTRA_WC_OPTION_PREFIX . 'enable_shipping_update', 'no' ) ) { 47 47 return; 48 48 } -
goultra-for-woocommerce/trunk/includes/class-goultra-subscribers.php
r3603449 r3617733 52 52 add_action( 'wp_ajax_goultra_backfill_subscribers', array( $this, 'ajax_backfill' ) ); 53 53 add_action( 'wp_ajax_goultra_save_ml_settings', array( $this, 'ajax_save_ml_settings' ) ); 54 add_action( 'wp_ajax_goultra_add_subscriber', array( $this, 'ajax_add_subscriber' ) ); 55 add_action( 'wp_ajax_goultra_edit_subscriber_name', array( $this, 'ajax_edit_name' ) ); 54 56 55 57 if ( ! get_option( GOULTRA_WC_OPTION_PREFIX . 'connected', false ) ) { … … 132 134 update_option( GOULTRA_WC_OPTION_PREFIX . 'ml_include_email', ( '1' === ( $_POST['include_email'] ?? '' ) ) ? 'yes' : 'no' ); 133 135 wp_send_json_success(); 136 } 137 138 /** 139 * Manually add a contact to the marketing list (typically the store owner's 140 * own number, so a campaign can be tested by selecting only that contact). 141 */ 142 public function ajax_add_subscriber() { 143 check_ajax_referer( 'goultra_admin_nonce', 'nonce' ); 144 if ( ! current_user_can( 'manage_woocommerce' ) ) { 145 wp_send_json_error( array( 'message' => __( 'Permission denied.', 'goultra-for-woocommerce' ) ) ); 146 } 147 if ( ! get_option( GOULTRA_WC_OPTION_PREFIX . 'connected', false ) ) { 148 wp_send_json_error( array( 'message' => __( 'Connect your store first.', 'goultra-for-woocommerce' ) ) ); 149 } 150 151 $digits = GoUltra_API::to_e164_digits( sanitize_text_field( wp_unslash( $_POST['phone'] ?? '' ) ) ); 152 $name = sanitize_text_field( wp_unslash( $_POST['name'] ?? '' ) ); 153 if ( '' === $digits ) { 154 wp_send_json_error( array( 'message' => __( 'Phone number is required.', 'goultra-for-woocommerce' ) ) ); 155 } 156 157 $api = GoUltra_API::instance(); 158 $api->refresh_api_key(); 159 $res = $api->push_consent( array( array( 160 'phone' => '+' . $digits, 161 'name' => $name, 162 'state' => 'granted', 163 'source' => 'woo_manual', 164 ) ) ); 165 166 if ( empty( $res['success'] ) ) { 167 wp_send_json_error( array( 'message' => __( 'Could not add the contact. Please try again.', 'goultra-for-woocommerce' ) ) ); 168 } 169 wp_send_json_success(); 170 } 171 172 /** 173 * Edit a subscriber's display name (name only) and sync it to GoUltra. 174 */ 175 public function ajax_edit_name() { 176 check_ajax_referer( 'goultra_admin_nonce', 'nonce' ); 177 if ( ! current_user_can( 'manage_woocommerce' ) ) { 178 wp_send_json_error( array( 'message' => __( 'Permission denied.', 'goultra-for-woocommerce' ) ) ); 179 } 180 if ( ! get_option( GOULTRA_WC_OPTION_PREFIX . 'connected', false ) ) { 181 wp_send_json_error( array( 'message' => __( 'Connect your store first.', 'goultra-for-woocommerce' ) ) ); 182 } 183 $phone = sanitize_text_field( wp_unslash( $_POST['phone'] ?? '' ) ); 184 $name = sanitize_text_field( wp_unslash( $_POST['name'] ?? '' ) ); 185 if ( '' === $phone ) { 186 wp_send_json_error( array( 'message' => __( 'Phone number is required.', 'goultra-for-woocommerce' ) ) ); 187 } 188 $api = GoUltra_API::instance(); 189 $api->refresh_api_key(); 190 $res = $api->update_subscriber( $phone, $name ); 191 if ( empty( $res['success'] ) ) { 192 wp_send_json_error( array( 'message' => __( 'Could not update the name. Please try again.', 'goultra-for-woocommerce' ) ) ); 193 } 194 // The backend returns HTTP 200 with { updated: false } when the number is 195 // not on the marketing list (nothing to rename). Surface that clearly 196 // instead of pretending the edit succeeded. 197 $body = ( isset( $res['response'] ) && is_array( $res['response'] ) ) ? $res['response'] : array(); 198 if ( array_key_exists( 'updated', $body ) && empty( $body['updated'] ) ) { 199 $msg = ( ! empty( $body['message'] ) && is_string( $body['message'] ) ) 200 ? $body['message'] 201 : __( 'This number is not on your marketing list, so there is no name to update.', 'goultra-for-woocommerce' ); 202 wp_send_json_error( array( 'message' => $msg ) ); 203 } 204 wp_send_json_success( array( 'name' => $name ) ); 134 205 } 135 206 -
goultra-for-woocommerce/trunk/includes/class-goultra-templates.php
r3588005 r3617733 122 122 'abandoned_cart_2' => 'Abandoned Cart - Step 2 (Urgency)', 123 123 'abandoned_cart_3' => 'Abandoned Cart - Step 3 (Discount)', 124 'marketing_offer' => 'Marketing Offer', 124 125 'custom' => 'Custom Template', 125 126 ); … … 140 141 add_action( 'wp_ajax_goultra_fetch_remote_templates', array( $this, 'ajax_fetch_remote_templates' ) ); 141 142 add_action( 'wp_ajax_goultra_sync_template_statuses', array( $this, 'ajax_sync_template_statuses' ) ); 143 add_action( 'wp_ajax_goultra_submit_rich_template', array( $this, 'ajax_submit_rich_template' ) ); 144 } 145 146 /** 147 * Canonical per-language opt-out footer (the exact wording the GoUltra 148 * backend's unsubscribe auto-detect recognizes). Marketing templates that 149 * use the footer opt-out method get this locked in. 150 * 151 * @param string $lang Locale or 2-letter code. 152 * @return string 153 */ 154 public static function optout_footer( $lang ) { 155 $map = array( 156 'en' => 'Reply STOP to unsubscribe', 157 'he' => 'להסרה השב הסר', 158 'ar' => 'للإزالة أرسل قف', 159 'es' => 'Responda PARAR para darse de baja', 160 'fr' => 'Répondez STOP pour vous désinscrire', 161 'de' => 'Antworten Sie STOPP zum Abbestellen', 162 'it' => 'Rispondi STOP per disiscriverti', 163 'pt' => 'Responda PARAR para cancelar', 164 'ru' => 'Ответьте СТОП для отписки', 165 'tr' => 'Aboneliği iptal etmek için DUR yazın', 166 'nl' => 'Antwoord STOP om af te melden', 167 'pl' => 'Wyślij STOP aby zrezygnować', 168 'uk' => 'Відповідайте СТОП для відписки', 169 'id' => 'Balas STOP untuk berhenti berlangganan', 170 'ms' => 'Balas STOP untuk berhenti melanggan', 171 'vi' => 'Trả lời STOP để hủy đăng ký', 172 'th' => 'ตอบ STOP เพื่อยกเลิกการสมัคร', 173 'ja' => '配信停止には STOP と返信してください', 174 'ko' => '구독 취소하려면 STOP 회신', 175 'zh' => '回复 STOP 取消订阅', 176 'hi' => 'सदस्यता रद्द करने के लिए STOP जवाब दें', 177 ); 178 $code = strtolower( substr( (string) $lang, 0, 2 ) ); 179 return isset( $map[ $code ] ) ? $map[ $code ] : $map['en']; 180 } 181 182 /** 183 * AJAX: submit a rich custom / marketing template. 184 * 185 * Sends the template through the WooCommerce template path 186 * (POST /api/v1/wc/templates/submit). That endpoint accepts the store's 187 * connected key (per-number / gk_ / sk_live_), resolves the linked WhatsApp 188 * number's WABA, converts the named {{vars}} to numbered {{1}} + builds the 189 * variable mapping, and creates the Meta template with header type, footer 190 * opt-out and button. We do NOT call /api/meta-templates directly because it 191 * only accepts the account sk_live_ key, which most connected stores do not 192 * use (that mismatch is what produced the "Missing API key" error). 193 */ 194 public function ajax_submit_rich_template() { 195 check_ajax_referer( 'goultra_admin_nonce', 'nonce' ); 196 if ( ! current_user_can( 'manage_woocommerce' ) ) { 197 wp_send_json_error( array( 'message' => __( 'Permission denied.', 'goultra-for-woocommerce' ) ) ); 198 } 199 200 $lang = sanitize_text_field( wp_unslash( $_POST['language'] ?? 'en' ) ); 201 $category = ( 'MARKETING' === ( $_POST['category'] ?? '' ) ) ? 'MARKETING' : 'UTILITY'; 202 $header_type = strtoupper( sanitize_text_field( wp_unslash( $_POST['header_type'] ?? 'NONE' ) ) ); 203 if ( ! in_array( $header_type, array( 'TEXT', 'IMAGE', 'VIDEO', 'DOCUMENT' ), true ) ) { 204 $header_type = 'NONE'; 205 } 206 $header_text = 'TEXT' === $header_type ? sanitize_text_field( wp_unslash( $_POST['header_text'] ?? '' ) ) : ''; 207 $body = trim( (string) wp_unslash( $_POST['body'] ?? '' ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- template body, sanitized below via sanitize_textarea_field 208 $body = sanitize_textarea_field( $body ); 209 $footer_in = sanitize_text_field( wp_unslash( $_POST['footer'] ?? '' ) ); 210 $opt_method = 'opt_out_button' === ( $_POST['opt_out_method'] ?? '' ) ? 'opt_out_button' : 'footer_text'; 211 $name_in = sanitize_text_field( wp_unslash( $_POST['name'] ?? '' ) ); 212 // Preset style (sale / flash_sale / holiday / launch / back_in_stock). Sent for 213 // marketing presets so the backend embeds it in the template name, exactly like 214 // the standard types (woo_marketing_offer_{style}_{lang}_{ts}) - that is what 215 // makes the per-style Active template state work after a status sync. 216 $style_key = substr( preg_replace( '/[^a-z0-9_]/', '', strtolower( (string) wp_unslash( $_POST['style_key'] ?? '' ) ) ), 0, 30 ); 217 // MARKETING templates written in the custom builder (no preset style) are 218 // marked style 'custom': the backend embeds it in the name 219 // (woo_marketing_offer_custom_{lang}_{ts}) so even after a status sync the 220 // row stays identifiable as builder-made and shows in the custom list, and 221 // it can never be confused with a preset style card. 222 if ( '' === $style_key && 'MARKETING' === $category ) { 223 $style_key = 'custom'; 224 } 225 $buttons_in = json_decode( wp_unslash( $_POST['buttons'] ?? '[]' ), true ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized 226 227 $len = function_exists( 'mb_strlen' ) ? mb_strlen( $body ) : strlen( $body ); 228 if ( $len < 5 || $len > 1024 ) { 229 wp_send_json_error( array( 'message' => __( 'The message text must be between 5 and 1024 characters.', 'goultra-for-woocommerce' ) ) ); 230 } 231 if ( preg_match( '/\}\}\s*\{\{/', $body ) ) { 232 wp_send_json_error( array( 'message' => __( 'Two variables cannot be next to each other. Add some text between them (Meta rejects it otherwise).', 'goultra-for-woocommerce' ) ) ); 233 } 234 235 // The WC submit endpoint converts named {{first_name}} -> numbered {{1}} 236 // and builds variable_mapping + examples itself, so we send the NAMED body. 237 238 // Buttons: normalize to [{type,text,value}], max 10, drop empties. 239 $buttons = array(); 240 if ( is_array( $buttons_in ) ) { 241 foreach ( array_slice( $buttons_in, 0, 10 ) as $b ) { 242 if ( ! is_array( $b ) ) { 243 continue; 244 } 245 $type = strtoupper( (string) ( $b['type'] ?? '' ) ); 246 $text = trim( mb_substr( (string) ( $b['text'] ?? '' ), 0, 25 ) ); 247 $val = trim( (string) ( $b['value'] ?? '' ) ); 248 if ( '' === $text ) { 249 continue; 250 } 251 if ( 'QUICK_REPLY' === $type ) { 252 $buttons[] = array( 'type' => 'QUICK_REPLY', 'text' => $text, 'value' => '' ); 253 } elseif ( 'URL' === $type ) { 254 if ( '' !== $val && ! preg_match( '#^https?://#i', $val ) ) { 255 $val = 'https://' . $val; 256 } 257 if ( preg_match( '#^https?://#i', $val ) ) { 258 $buttons[] = array( 'type' => 'URL', 'text' => $text, 'value' => $val ); 259 } 260 } elseif ( 'PHONE_NUMBER' === $type ) { 261 if ( preg_match( '/^[+\d][\d\s-]{5,}$/', $val ) ) { 262 $buttons[] = array( 'type' => 'PHONE_NUMBER', 'text' => $text, 'value' => $val ); 263 } 264 } 265 } 266 } 267 268 // Opt-out enforcement for MARKETING. 269 $footer = mb_substr( $footer_in, 0, 60 ); 270 $include_unsubscribe = 0; 271 $unsub_method = 'footer_text'; 272 if ( 'MARKETING' === $category ) { 273 if ( 'opt_out_button' === $opt_method ) { 274 $include_unsubscribe = 1; 275 $unsub_method = 'opt_out_button'; 276 } elseif ( '' === $footer || $footer === self::optout_footer( $lang ) ) { 277 $footer = self::optout_footer( $lang ); 278 } 279 } 280 281 // Template name: lowercase [a-z0-9_], auto-prefixed + timestamped. 282 $base = preg_replace( '/[^a-z0-9_]+/', '_', strtolower( $name_in ) ); 283 $base = trim( $base, '_' ); 284 $base = '' !== $base ? substr( $base, 0, 40 ) : ( 'MARKETING' === $category ? 'marketing_offer' : 'custom' ); 285 $name = 'woo_' . $base . '_' . preg_replace( '/[^a-z0-9]/', '', strtolower( $lang ) ) . '_' . time(); 286 287 $template_key = ( 'MARKETING' === $category ) ? 'marketing_offer' : 'custom_' . time(); 288 289 // Submit via the WooCommerce template path (POST /api/v1/wc/templates/submit). 290 // That endpoint accepts the store's connected key (per-number / gk_ / sk_live_) 291 // and resolves the linked WhatsApp number's WABA itself, then internally 292 // creates the Meta template with the right config. It ALSO converts the named 293 // {{first_name}} variables to numbered {{1}} and builds the variable_mapping, 294 // so we send the body with NAMED variables and let the backend map them. 295 // (The rich /api/meta-templates endpoint only accepts the account sk_live_ key, 296 // which most connected stores do not use, so we do not call it from the plugin.) 297 $payload = array( 298 'template_key' => $template_key, 299 'language' => $lang, 300 'category' => $category, 301 // Same field the standard (saved-row) submit already sends; the backend 302 // embeds it in the Meta template name. Ignored when empty/unsupported. 303 'style_key' => $style_key ?: null, 304 'body_text' => $body, 305 'header_type' => $header_type, 306 'header_text' => $header_text, 307 'footer_text' => $footer, 308 // First URL button (the WC path currently maps a single URL button). 309 'button_text' => $buttons ? $buttons[0]['text'] : '', 310 'button_url' => ( $buttons && 'URL' === $buttons[0]['type'] ) ? $buttons[0]['value'] : '', 311 // Full button set + opt-out method, forwarded for backends that support 312 // them (ignored where not yet supported). 313 'buttons' => $buttons ? wp_json_encode( $buttons ) : null, 314 'include_unsubscribe' => $include_unsubscribe, 315 'unsubscribe_method' => $unsub_method, 316 ); 317 318 $api = GoUltra_API::instance(); 319 $api->refresh_api_key(); 320 321 $res = $api->submit_template( $payload ); 322 $via = 'wc/templates/submit'; 323 if ( empty( $res['success'] ) ) { 324 wp_send_json_error( array( 325 'message' => sprintf( 326 /* translators: %s: API error reason */ 327 __( 'The template could not be submitted. %s', 'goultra-for-woocommerce' ), 328 $this->extract_error( $res ) 329 ), 330 ) ); 331 } 332 333 // Cache locally so it appears as pending + shows in the campaign picker. 334 $tpl_name = ''; 335 $body_r = is_array( $res['response'] ?? null ) ? $res['response'] : ( is_array( $res ) ? $res : array() ); 336 foreach ( array( 'template_name', 'name' ) as $k ) { 337 if ( ! empty( $body_r[ $k ] ) ) { 338 $tpl_name = (string) $body_r[ $k ]; 339 break; 340 } 341 } 342 if ( '' === $tpl_name ) { 343 $tpl_name = $name; 344 } 345 global $wpdb; 346 $table = $wpdb->prefix . 'goultra_templates'; 347 $wpdb->insert( $table, array( 348 'template_key' => ( 'MARKETING' === $category ) ? 'marketing_offer' : 'custom_' . time(), 349 'language' => $lang, 350 'template_name' => $tpl_name, 351 'style_key' => $style_key ?: 'custom', 352 'header_type' => 'IMAGE' === $header_type ? 'image' : ( 'TEXT' === $header_type ? 'text' : 'none' ), 353 'header_text' => $header_text, 354 'body_text' => $body, 355 'footer_text' => $footer, 356 'button_text' => $buttons ? $buttons[0]['text'] : '', 357 'button_url' => ( $buttons && 'URL' === $buttons[0]['type'] ) ? $buttons[0]['value'] : '', 358 'status' => 'pending', 359 ) ); 360 361 wp_send_json_success( array( 'template_name' => $tpl_name, 'via' => $via ) ); 362 } 363 364 /** Pull a human error string out of an API result. */ 365 private function extract_error( $res ) { 366 $body = is_array( $res['response'] ?? null ) ? $res['response'] : array(); 367 foreach ( array( 'error', 'error_code', 'message', 'details' ) as $k ) { 368 if ( ! empty( $res[ $k ] ) && is_string( $res[ $k ] ) ) { 369 return GoUltra_API::friendly_error( $res[ $k ] ) ?: $res[ $k ]; 370 } 371 if ( ! empty( $body[ $k ] ) && is_string( $body[ $k ] ) ) { 372 return GoUltra_API::friendly_error( $body[ $k ] ) ?: $body[ $k ]; 373 } 374 } 375 return __( 'Unknown error.', 'goultra-for-woocommerce' ); 142 376 } 143 377 … … 430 664 // broke the saved per-status template selections (toggle stayed on, template dropped). 431 665 432 $woo_like = "( template_key LIKE 'order_%' OR template_key LIKE 'shipping_%' OR template_key LIKE 'admin_%' OR template_key LIKE 'abandoned_%' OR template_key LIKE ' custom%' )";666 $woo_like = "( template_key LIKE 'order_%' OR template_key LIKE 'shipping_%' OR template_key LIKE 'admin_%' OR template_key LIKE 'abandoned_%' OR template_key LIKE 'marketing_%' OR template_key LIKE 'custom%' )"; 433 667 434 668 // Existing woo_* rows: template_name => id. Unnamed or duplicate-name rows are queued … … 444 678 445 679 $wc_types = array( 'order_confirmation','shipping_update','admin_new_order', 446 'abandoned_cart_1','abandoned_cart_2','abandoned_cart_3' );680 'abandoned_cart_1','abandoned_cart_2','abandoned_cart_3','marketing_offer' ); 447 681 448 682 $seen_names = array(); … … 469 703 // Extract style_key from template_name if not provided by API 470 704 // Format: woo_{type}_{style}_{lang}_{ts} e.g. woo_order_confirmation_friendly_he_177... 705 // Marketing styles can carry underscores (flash_sale, back_in_stock), so match 706 // the longest known style at the START of the remainder (longest-first list) 707 // instead of only its first "_" segment. 471 708 $style_key = $r['style_key'] ?? ''; 472 709 if ( empty( $style_key ) && ! empty( $tpl_key ) && ! empty( $name ) ) { 473 $known_styles = array( 'friendly', 'professional', 'short', 'direct', 'supportive', 'detailed' ); 710 $known_styles = array( 711 'back_in_stock', 'professional', 'flash_sale', 'supportive', 'detailed', 712 'friendly', 'holiday', 'custom', 'direct', 'launch', 'short', 'sale', 713 ); 474 714 $prefix = 'woo_' . $tpl_key . '_'; 475 715 if ( strpos( $name, $prefix ) === 0 ) { 476 716 $remainder = substr( $name, strlen( $prefix ) ); 477 $parts = explode( '_', $remainder ); 478 if ( ! empty( $parts[0] ) && in_array( $parts[0], $known_styles, true ) ) { 479 $style_key = $parts[0]; 717 foreach ( $known_styles as $ks ) { 718 if ( strpos( $remainder, $ks . '_' ) === 0 ) { 719 $style_key = $ks; 720 break; 721 } 480 722 } 481 723 } -
goultra-for-woocommerce/trunk/languages/goultra-for-woocommerce-ar.po
r3603449 r3617733 925 925 msgid "Send a test message (optional)" 926 926 msgstr "إرسال رسالة تجريبية (اختياري)" 927 928 msgid "Campaigns" 929 msgstr "الحملات" 930 931 msgid "New campaign" 932 msgstr "حملة جديدة" 933 934 msgid "Compose a WhatsApp campaign for your Marketing List subscribers. Unsubscribed contacts are always filtered out automatically." 935 msgstr "أنشئ حملة واتساب لمشتركي قائمتك التسويقية. تتم تصفية جهات الاتصال الملغاة تلقائياً دائماً." 936 937 msgid "Connect your store to GoUltra to send campaigns." 938 msgstr "اربط متجرك بـ GoUltra لإرسال الحملات." 939 940 msgid "Template" 941 msgstr "القالب" 942 943 msgid "Refresh" 944 msgstr "تحديث" 945 946 msgid "No approved marketing templates yet. Create one below." 947 msgstr "لا توجد قوالب تسويقية معتمدة بعد. أنشئ واحداً أدناه." 948 949 msgid "Variables" 950 msgstr "المتغيرات" 951 952 msgid "Header image" 953 msgstr "صورة الترويسة" 954 955 msgid "Media Library" 956 msgstr "مكتبة الوسائط" 957 958 msgid "Pick a product" 959 msgstr "اختيار منتج" 960 961 msgid "Search products" 962 msgstr "البحث في المنتجات" 963 964 msgid "Send a test to your phone first" 965 msgstr "أرسل تجربة إلى هاتفك أولاً" 966 967 msgid "Send test" 968 msgstr "إرسال تجربة" 969 970 msgid "Test sent. Check your WhatsApp." 971 msgstr "تم إرسال التجربة. تحقق من واتساب لديك." 972 973 msgid "Audience" 974 msgstr "الجمهور" 975 976 msgid "When to send" 977 msgstr "موعد الإرسال" 978 979 msgid "Send now" 980 msgstr "إرسال الآن" 981 982 msgid "Schedule for later" 983 msgstr "جدولة لوقت لاحق" 984 985 msgid "Launch campaign" 986 msgstr "إطلاق الحملة" 987 988 msgid "online" 989 msgstr "متصل" 990 991 msgid "Choose a template to see the preview." 992 msgstr "اختر قالباً لرؤية المعاينة." 993 994 msgid "Campaign history" 995 msgstr "سجل الحملات" 996 997 msgid "Sent" 998 msgstr "أُرسلت" 999 1000 msgid "Failed" 1001 msgstr "فشلت" 1002 1003 msgid "Create a marketing template" 1004 msgstr "إنشاء قالب تسويقي" 1005 1006 msgid "Pick a language and style, adjust the text, and submit for Meta approval. A localized opt-out footer is added automatically, as Meta requires for marketing." 1007 msgstr "اختر لغةً ونمطاً، عدّل النص وأرسله لموافقة Meta. تُضاف سطر إلغاء الاشتراك بلغة القالب تلقائياً كما تشترط Meta للتسويق." 1008 1009 msgid "Language" 1010 msgstr "اللغة" 1011 1012 msgid "Style" 1013 msgstr "النمط" 1014 1015 msgid "Message text" 1016 msgstr "نص الرسالة" 1017 1018 msgid "Button label (optional)" 1019 msgstr "نص الزر (اختياري)" 1020 1021 msgid "Button link (optional)" 1022 msgstr "رابط الزر (اختياري)" 1023 1024 msgid "Submit for approval" 1025 msgstr "إرسال للموافقة" 1026 1027 msgid "Something went wrong. Please try again." 1028 msgstr "حدث خطأ ما. حاول مرة أخرى." 1029 1030 msgid "Choose a template" 1031 msgstr "اختر قالباً" 1032 1033 msgid "Choose a template first." 1034 msgstr "اختر قالباً أولاً." 1035 1036 msgid "Awaiting Meta approval:" 1037 msgstr "بانتظار موافقة Meta:" 1038 1039 msgid "Customer first name" 1040 msgstr "الاسم الأول للعميل" 1041 1042 msgid "Customer full name" 1043 msgstr "الاسم الكامل للعميل" 1044 1045 msgid "Fixed value" 1046 msgstr "قيمة ثابتة" 1047 1048 msgid "No products with a usable image were found." 1049 msgstr "لم يُعثر على منتجات بصورة مناسبة." 1050 1051 msgid "Sending..." 1052 msgstr "جارٍ الإرسال..." 1053 1054 msgid "Please fill in all template variables." 1055 msgstr "يرجى تعبئة جميع متغيرات القالب." 1056 1057 msgid "This template needs a header image. Choose one from the media library, a product, or paste an https image link." 1058 msgstr "يحتاج هذا القالب إلى صورة ترويسة. اختر من مكتبة الوسائط أو من منتج أو الصق رابط https للصورة." 1059 1060 msgid "%d subscribers will receive this campaign." 1061 msgstr "سيتلقى هذه الحملة %d مشتركاً." 1062 1063 msgid "Send this campaign to %d subscribers now?" 1064 msgstr "هل تريد إرسال هذه الحملة إلى %d مشتركاً الآن؟" 1065 1066 msgid "Schedule this campaign to %d subscribers?" 1067 msgstr "هل تريد جدولة هذه الحملة إلى %d مشتركاً؟" 1068 1069 msgid "Campaign launched. Messages are on their way." 1070 msgstr "انطلقت الحملة. الرسائل في طريقها." 1071 1072 msgid "Campaign scheduled." 1073 msgstr "تمت جدولة الحملة." 1074 1075 msgid "No campaigns yet." 1076 msgstr "لا توجد حملات بعد." 1077 1078 msgid "Pause" 1079 msgstr "إيقاف مؤقت" 1080 1081 msgid "Cancel" 1082 msgstr "إلغاء" 1083 1084 msgid "Pause this campaign?" 1085 msgstr "هل تريد إيقاف الحملة مؤقتاً؟" 1086 1087 msgid "Cancel this scheduled campaign?" 1088 msgstr "هل تريد إلغاء الحملة المجدولة؟" 1089 1090 msgid "Processing" 1091 msgstr "قيد المعالجة" 1092 1093 msgid "Scheduled" 1094 msgstr "مجدولة" 1095 1096 msgid "Completed" 1097 msgstr "اكتملت" 1098 1099 msgid "Cancelled" 1100 msgstr "أُلغيت" 1101 1102 msgid "Paused" 1103 msgstr "موقوفة مؤقتاً" 1104 1105 msgid "Opt-out footer (added automatically):" 1106 msgstr "سطر إلغاء الاشتراك (يُضاف تلقائياً):" 1107 1108 msgid "The message text must be between 15 and 1024 characters." 1109 msgstr "يجب أن يكون نص الرسالة بين 15 و1024 حرفاً." 1110 1111 msgid "Template submitted. Meta approval usually takes minutes to a few hours." 1112 msgstr "تم إرسال القالب. موافقة Meta تستغرق عادة من دقائق إلى بضع ساعات." 1113 1114 msgid "A button needs both a label and a link." 1115 msgstr "يحتاج الزر إلى نص ورابط معاً." 1116 1117 msgid "Could not submit the template. Please try again." 1118 msgstr "تعذّر إرسال القالب. حاول مرة أخرى." 1119 1120 msgid "Could not load templates. Please try again." 1121 msgstr "تعذّر تحميل القوالب. حاول مرة أخرى." 1122 1123 msgid "Test send failed. Please try again." 1124 msgstr "فشل إرسال التجربة. حاول مرة أخرى." 1125 1126 msgid "Could not launch the campaign. Please try again." 1127 msgstr "تعذّر إطلاق الحملة. حاول مرة أخرى." 1128 1129 msgid "The action failed. Please try again." 1130 msgstr "فشلت العملية. حاول مرة أخرى." 1131 1132 msgid "Invalid request." 1133 msgstr "طلب غير صالح." 1134 1135 msgid "Choose a valid date and time for the scheduled send." 1136 msgstr "اختر تاريخاً ووقتاً صالحين للإرسال المجدول." 1137 1138 msgid "A campaign was just launched. Please wait two minutes before launching another." 1139 msgstr "أُطلقت حملة للتو. انتظر دقيقتين قبل إطلاق أخرى." 1140 1141 msgid "Your audience is larger than 5,000 contacts. Please run this campaign from the GoUltra dashboard." 1142 msgstr "جمهورك أكبر من 5,000 جهة اتصال. شغّل هذه الحملة من لوحة تحكم GoUltra." 1143 1144 msgid "Your GoUltra account has more than one WhatsApp number connected, so the sending number must be chosen in the GoUltra dashboard for this store." 1145 msgstr "حسابك في GoUltra موصول بأكثر من رقم واتساب، لذا يجب اختيار رقم الإرسال لهذا المتجر من لوحة تحكم GoUltra." 1146 1147 msgid "Your GoUltra plan message allowance for this cycle has been reached. Upgrade your plan to send more." 1148 msgstr "استُهلكت حصة الرسائل في خطة GoUltra لهذه الدورة. رقِّ خطتك لإرسال المزيد." 1149 1150 msgid "GoUltra for WooCommerce - WhatsApp Marketing, Notifications & Abandoned Cart" 1151 msgstr "GoUltra لـ WooCommerce - تسويق وإشعارات وسلة متروكة عبر WhatsApp" 1152 1153 msgid "WooCommerce WhatsApp notifications: automated order, shipping and abandoned-cart messages, plus opt-in marketing campaigns, powered by GoUltra." 1154 msgstr "إشعارات WhatsApp لـ WooCommerce: رسائل تلقائية لتأكيد الطلب والشحن والسلة المتروكة، بالإضافة إلى حملات تسويقية بموافقة، بواسطة GoUltra." 1155 1156 msgid "Marketing Offer" 1157 msgstr "عرض تسويقي" 1158 1159 msgid "Promotions and offers for opted-in subscribers" 1160 msgstr "عروض وتخفيضات للمشتركين الموافقين" 1161 1162 msgid "Recipients" 1163 msgstr "المستلمون" 1164 1165 msgid "All subscribers" 1166 msgstr "جميع المشتركين" 1167 1168 msgid "Choose specific contacts" 1169 msgstr "اختيار جهات اتصال محددة" 1170 1171 msgid "Search name or number" 1172 msgstr "ابحث بالاسم أو الرقم" 1173 1174 msgid "Select all" 1175 msgstr "تحديد الكل" 1176 1177 msgid "Clear" 1178 msgstr "مسح" 1179 1180 msgid "Also send to my own number (%s) for a test" 1181 msgstr "أرسل أيضاً إلى رقمي (%s) للاختبار" 1182 1183 msgid "Also send to my own number for a test (set your admin number in Settings)" 1184 msgstr "أرسل أيضاً إلى رقمي للاختبار (عيّن رقم المدير في الإعدادات)" 1185 1186 msgid "Need a marketing template?" 1187 msgstr "تحتاج قالباً تسويقياً؟" 1188 1189 msgid "Create one in the Templates tab (Marketing Offer): pick a language and ready-made style, and the opt-out footer is added automatically. Approved templates appear in the picker above." 1190 msgstr "أنشئه من تبويب القوالب (عرض تسويقي): اختر لغة ونمطاً جاهزاً، وتُضاف سطر إلغاء الاشتراك تلقائياً. تظهر القوالب المعتمدة في المُنتقي أعلاه." 1191 1192 msgid "Open Templates" 1193 msgstr "فتح القوالب" 1194 1195 msgid "Delivery receipts and product sync" 1196 msgstr "إيصالات التسليم ومزامنة المنتجات" 1197 1198 msgid "These work automatically once your store is connected. There is nothing to set up." 1199 msgstr "تعمل هذه تلقائياً بمجرد ربط متجرك. لا يوجد ما يتطلب الإعداد." 1200 1201 msgid "Connected and active." 1202 msgstr "متصل ونشط." 1203 1204 msgid "Message delivery receipts appear as order notes, and your product catalog is available to GoUltra automatically. No secret or URL to copy anywhere." 1205 msgstr "تظهر إيصالات تسليم الرسائل كملاحظات على الطلب، ويكون كتالوج منتجاتك متاحاً لـ GoUltra تلقائياً. لا يوجد سر أو رابط لنسخه." 1206 1207 msgid "Technical details (for support)" 1208 msgstr "تفاصيل تقنية (للدعم)" 1209 1210 msgid "Webhook secret" 1211 msgstr "سر Webhook" 1212 1213 msgid "The plugin shares the secret with GoUltra automatically on connect, so you never paste it anywhere." 1214 msgstr "يشارك الإضافة السر مع GoUltra تلقائياً عند الاتصال، لذا لا تحتاج إلى لصقه في أي مكان." 1215 1216 msgid "All subscribers (%d)" 1217 msgstr "جميع المشتركين (%d)" 1218 1219 msgid "%d selected" 1220 msgstr "%d محدد" 1221 1222 msgid "No subscribers yet." 1223 msgstr "لا يوجد مشتركون بعد." 1224 1225 msgid "No recipients selected. Choose subscribers, or add your own number to send yourself a test." 1226 msgstr "لم يتم اختيار مستلمين. اختر مشتركين، أو أضف رقمك لإرسال اختبار لنفسك." 1227 1228 msgid "%d recipients will receive this campaign." 1229 msgstr "سيتلقى هذه الحملة %d مستلماً." 1230 1231 msgid "Send this campaign to %d recipients now?" 1232 msgstr "هل تريد إرسال هذه الحملة إلى %d مستلماً الآن؟" 1233 1234 msgid "Schedule this campaign to %d recipients?" 1235 msgstr "هل تريد جدولة هذه الحملة إلى %d مستلماً؟" 1236 1237 msgid "Add my number" 1238 msgstr "إضافة رقمي" 1239 1240 msgid "Add to the list" 1241 msgstr "إضافة إلى القائمة" 1242 1243 msgid "Adding..." 1244 msgstr "جارٍ الإضافة..." 1245 1246 msgid "Added to the list." 1247 msgstr "تمت الإضافة إلى القائمة." 1248 1249 msgid "Could not add the contact. Please try again." 1250 msgstr "تعذّرت إضافة جهة الاتصال. حاول مرة أخرى." 1251 1252 msgid "Adds the number to the marketing list as a subscribed contact. Useful for sending yourself a test campaign: on the Campaigns screen choose \"Choose specific contacts\" and select only your number." 1253 msgstr "يضيف الرقم إلى قائمة التسويق كجهة اتصال مشترِكة. مفيد لإرسال حملة تجريبية لنفسك: في شاشة الحملات اختر \"اختيار جهات اتصال محددة\" وحدد رقمك فقط." 1254 1255 msgid "Tip: to test on yourself, add your own number on the Marketing List screen, then select only it here." 1256 msgstr "نصيحة: للاختبار على نفسك، أضف رقمك في شاشة قائمة التسويق ثم حدده هنا فقط." 1257 1258 msgid "No recipients selected. Choose contacts from the list." 1259 msgstr "لم يتم اختيار مستلمين. اختر جهات اتصال من القائمة." 1260 1261 msgid "No name" 1262 msgstr "بدون اسم" 1263 1264 msgid "Today" 1265 msgstr "اليوم" 1266 1267 msgid "Your GoUltra account is currently on hold, so messages cannot be sent. Please contact GoUltra support to resume." 1268 msgstr "حساب GoUltra الخاص بك موقوف حالياً، لذا لا يمكن إرسال الرسائل. يرجى التواصل مع دعم GoUltra للاستئناف." 1269 1270 msgid "Edit name" 1271 msgstr "تعديل الاسم" 1272 1273 msgid "Schedule campaign" 1274 msgstr "جدولة الحملة" 1275 1276 msgid "Will be sent on %1$s at %2$s (your store time)." 1277 msgstr "سيُرسَل في %1$s الساعة %2$s (بتوقيت متجرك)." 1278 1279 msgid "Could not update the name. Please try again." 1280 msgstr "تعذّر تحديث الاسم. حاول مرة أخرى." 1281 1282 msgid "GoUltra returned HTTP %d." 1283 msgstr "أعادت GoUltra رمز HTTP %d." 1284 1285 msgid "Template name" 1286 msgstr "اسم القالب" 1287 1288 msgid "Lowercase letters, numbers and underscores only." 1289 msgstr "أحرف إنجليزية صغيرة وأرقام وشرطة سفلية فقط." 1290 1291 msgid "Marketing (offers, promotions)" 1292 msgstr "تسويقي (عروض، تخفيضات)" 1293 1294 msgid "Utility (order, account updates)" 1295 msgstr "خدمي (طلب، تحديثات الحساب)" 1296 1297 msgid "No header" 1298 msgstr "بدون ترويسة" 1299 1300 msgid "Text" 1301 msgstr "نص" 1302 1303 msgid "Video" 1304 msgstr "فيديو" 1305 1306 msgid "Document" 1307 msgstr "مستند" 1308 1309 msgid "Header text" 1310 msgstr "نص الترويسة" 1311 1312 msgid "You will choose the actual image/video/file when you send a campaign." 1313 msgstr "ستختار الصورة/الفيديو/الملف الفعلي عند إرسال حملة." 1314 1315 msgid "Note:" 1316 msgstr "ملاحظة:" 1317 1318 msgid "Utility templates are for transactional messages only (order, shipping, account). Promotional content will be rejected by Meta." 1319 msgstr "القوالب الخدمية مخصصة للرسائل المعاملاتية فقط (طلب، شحن، حساب). سيرفض Meta المحتوى الترويجي." 1320 1321 msgid "Insert a variable" 1322 msgstr "إدراج متغير" 1323 1324 msgid "(filled in automatically at send time)" 1325 msgstr "(يُملأ تلقائياً عند الإرسال)" 1326 1327 msgid "Buttons" 1328 msgstr "الأزرار" 1329 1330 msgid "(up to 10, optional)" 1331 msgstr "(حتى 10، اختياري)" 1332 1333 msgid "Add button" 1334 msgstr "إضافة زر" 1335 1336 msgid "Opt-out method (required for marketing)" 1337 msgstr "طريقة إلغاء الاشتراك (إلزامية للتسويق)" 1338 1339 msgid "Footer text (recommended)" 1340 msgstr "نص التذييل (موصى به)" 1341 1342 msgid "Unsubscribe button" 1343 msgstr "زر إلغاء الاشتراك" 1344 1345 msgid "Footer" 1346 msgstr "التذييل" 1347 1348 msgid "(small gray line, optional)" 1349 msgstr "(سطر رمادي صغير، اختياري)" 1350 1351 msgid "Quick reply" 1352 msgstr "رد سريع" 1353 1354 msgid "URL link" 1355 msgstr "رابط URL" 1356 1357 msgid "Phone number" 1358 msgstr "رقم الهاتف" 1359 1360 msgid "Button text" 1361 msgstr "نص الزر" 1362 1363 msgid "A URL or phone button needs a value." 1364 msgstr "زر URL أو الهاتف يحتاج إلى قيمة." 1365 1366 msgid "Remove" 1367 msgstr "إزالة" 1368 1369 msgid "Two variables cannot be next to each other. Add some text between them (Meta rejects it otherwise)." 1370 msgstr "لا يمكن وضع متغيرين متجاورين. أضف نصاً بينهما (وإلا سيرفضه Meta)." 1371 1372 msgid "The message text must be between 5 and 1024 characters." 1373 msgstr "يجب أن يكون نص الرسالة بين 5 و1024 حرفاً." 1374 1375 msgid "Meta rejected the template. %1$s (fallback: %2$s)" 1376 msgstr "رفض Meta القالب. %1$s (بديل: %2$s)" 1377 1378 msgid "Unknown error." 1379 msgstr "خطأ غير معروف." 1380 1381 msgid "The template could not be submitted. %s" 1382 msgstr "تعذر إرسال القالب. %s" 1383 1384 msgid "This number is not on your marketing list, so there is no name to update." 1385 msgstr "هذا الرقم غير موجود في قائمتك التسويقية، لذا لا يوجد اسم لتحديثه." 1386 1387 msgid "Paste your API key (it starts with gk_) from the GoUltra Dashboard → Settings → API." 1388 msgstr "الصق مفتاح API (يبدأ بـ gk_) من لوحة GoUltra → الإعدادات → API." 1389 1390 msgid "Campaign name (optional)" 1391 msgstr "اسم الحملة (اختياري)" 1392 1393 msgid "e.g. Summer Sale - July" 1394 msgstr "مثال: تخفيضات الصيف - يوليو" 1395 1396 msgid "A name to recognize this campaign in your history." 1397 msgstr "اسم يساعدك على تمييز هذه الحملة في السجل." 1398 1399 msgid "Upload / Media Library" 1400 msgstr "رفع / مكتبة الوسائط" 1401 1402 msgid "Image selected" 1403 msgstr "تم اختيار الصورة" 1404 1405 msgid "Or paste an image URL" 1406 msgstr "أو الصق رابط صورة" 1407 1408 msgid "JPEG or PNG images only. Choose from your Media Library, pick a store product, or paste a URL." 1409 msgstr "صور JPEG أو PNG فقط. اختر من مكتبة الوسائط، أو اختر منتجاً من المتجر، أو الصق رابطاً." 1410 1411 msgid "Enter a valid WhatsApp number, including the country code." 1412 msgstr "أدخل رقم WhatsApp صالحاً، بما في ذلك رمز الدولة." 1413 1414 msgid "Too many code requests. Please wait a while before trying again." 1415 msgstr "طلبات كود كثيرة جداً. يرجى الانتظار قليلاً ثم المحاولة مجدداً." 1416 1417 msgid "Your WhatsApp verification template is still being approved by Meta (usually a few minutes, sometimes longer). Please try again shortly - nothing else is needed from you." 1418 msgstr "لا يزال قالب التحقق الخاص بك على WhatsApp قيد الموافقة من Meta (عادةً بضع دقائق، وأحياناً أطول). حاول مرة أخرى بعد قليل - لا حاجة لأي إجراء إضافي منك." 1419 1420 msgid "Could not send the code. Please check the number and try again." 1421 msgstr "تعذر إرسال الكود. تحقق من الرقم وحاول مجدداً." 1422 1423 msgid "We sent a 6-digit code to %s on WhatsApp. Enter it below." 1424 msgstr "أرسلنا رمزاً من 6 أرقام إلى %s على WhatsApp. أدخله أدناه." 1425 1426 msgid "The code expired. Please request a new one." 1427 msgstr "انتهت صلاحية الكود. اطلب رمزاً جديداً." 1428 1429 msgid "Too many wrong attempts. Please request a new code." 1430 msgstr "محاولات خاطئة كثيرة جداً. اطلب رمزاً جديداً." 1431 1432 msgid "Incorrect code. Attempts left: %d." 1433 msgstr "رمز غير صحيح. المحاولات المتبقية: %d." 1434 1435 msgid "Your number is verified. It is now your store manager number and can be selected as a campaign recipient for your own tests." 1436 msgstr "تم التحقق من رقمك. أصبح الآن رقم مدير المتجر، ويمكن اختياره كمستلم في الحملة لاختباراتك." 1437 1438 msgid "Store manager" 1439 msgstr "مدير المتجر" 1440 1441 msgid "Verify your number (recommended)" 1442 msgstr "تحقق من رقمك (موصى به)" 1443 1444 msgid "Confirm your WhatsApp number with a code. A verified number becomes your store manager number, which you can select as a campaign recipient to test your own campaigns." 1445 msgstr "أكّد رقم WhatsApp الخاص بك برمز. يصبح الرقم الموثّق رقم مدير المتجر، ويمكنك اختياره كمستلم في الحملة لاختبار حملاتك." 1446 1447 msgid "Verified" 1448 msgstr "موثّق" 1449 1450 msgid "Verify a different number" 1451 msgstr "التحقق من رقم آخر" 1452 1453 msgid "Send code" 1454 msgstr "إرسال الرمز" 1455 1456 msgid "Verify" 1457 msgstr "تحقق" 1458 1459 msgid "Resend code" 1460 msgstr "إعادة إرسال الرمز" 1461 1462 msgid "Verifying..." 1463 msgstr "جارٍ التحقق..." 1464 1465 msgid "Enter your WhatsApp number first." 1466 msgstr "أدخل رقم WhatsApp الخاص بك أولاً." 1467 1468 msgid "Enter the code you received." 1469 msgstr "أدخل الرمز الذي استلمته." 1470 1471 msgid "Could not send the code." 1472 msgstr "تعذر إرسال الرمز." 1473 1474 msgid "Incorrect code." 1475 msgstr "رمز غير صحيح." 1476 1477 msgid "Network error. Please try again." 1478 msgstr "خطأ في الشبكة. حاول مرة أخرى." 1479 1480 msgid "Building a template for a product?" 1481 msgstr "هل تبني قالباً لمنتج؟" 1482 1483 msgid "Pick a store product and we set up the template for it: an image header plus a \"View product\" button that links to it. Then choose a language and submit for approval." 1484 msgstr "اختر منتجاً من المتجر وسنجهّز القالب له: ترويسة صورة وزر \"عرض المنتج\" يرتبط به. ثم اختر لغة وأرسله للموافقة." 1485 1486 msgid "View product" 1487 msgstr "عرض المنتج" 1488 1489 msgid "No products found." 1490 msgstr "لم يتم العثور على منتجات." 1491 1492 msgid "You already have an approved or pending template of this type for this language. Create another one?" 1493 msgstr "لديك بالفعل قالب موافق عليه أو قيد الانتظار من هذا النوع بهذه اللغة. إنشاء واحد آخر؟" 1494 1495 msgid "Unconfirmed - verify before resending" 1496 msgstr "غير مؤكد - تحقق قبل إعادة الإرسال" 1497 1498 msgid "The connection dropped before we got a reply, so the campaign may or may not have gone out. Check Campaign History and your GoUltra dashboard before launching it again to avoid sending it twice." 1499 msgstr "انقطع الاتصال قبل أن نتلقى ردا، لذا قد يكون تم إرسال الحملة أو لا. تحقق من سجل الحملات ولوحة تحكم GoUltra قبل إطلاقها مرة أخرى لتجنب إرسالها مرتين." 1500 1501 msgid "GoUltra for WooCommerce: connection problem." 1502 msgstr "GoUltra لـ WooCommerce: مشكلة في الاتصال." 1503 1504 msgid "Recent WhatsApp messages were rejected because the API key is no longer valid (it may have been regenerated or the account changed). Messages are not being delivered." 1505 msgstr "تم رفض رسائل WhatsApp الأخيرة لأن مفتاح API لم يعد صالحا (ربما تم إنشاؤه من جديد أو تغير الحساب). الرسائل لا يتم تسليمها." 1506 1507 msgid "Reconnect your store" 1508 msgstr "أعد ربط متجرك" 1509 1510 msgid "Image" 1511 msgstr "صورة" 1512 1513 msgid "View" 1514 msgstr "عرض" 1515 1516 msgid "rejected" 1517 msgstr "مرفوضة" 1518 1519 msgid "No templates match your search." 1520 msgstr "لا توجد قوالب تطابق بحثك." 1521 1522 msgid "Search your templates by name, text or language..." 1523 msgstr "ابحث في قوالبك بالاسم أو النص أو اللغة..." 1524 1525 msgid "Close" 1526 msgstr "إغلاق" 1527 1528 msgid "GoUltra did not accept this store's key for this action. Your connection is fine (order messages work), but campaign sending for this number may not be enabled yet on your GoUltra account. Please contact GoUltra support to enable it." 1529 msgstr "لم تقبل GoUltra مفتاح هذا المتجر لهذا الإجراء. اتصالك سليم (رسائل الطلبات تُرسل)، لكن قد لا يكون إرسال الحملات لهذا الرقم مُفعّلًا بعد في حسابك على GoUltra. يرجى التواصل مع دعم GoUltra لتفعيله." 1530 1531 msgid "JPEG or PNG images only." 1532 msgstr "صور JPEG أو PNG فقط." 1533 1534 msgid "Marketing sending is turned off for this WhatsApp number. Enable marketing for this number in your GoUltra dashboard to send campaigns." 1535 msgstr "إرسال الرسائل التسويقية مُعطّل لرقم WhatsApp هذا. فعّل التسويق لهذا الرقم في لوحة تحكم GoUltra لإرسال الحملات." 1536 1537 msgid "Chat with us" 1538 msgstr "تحدث معنا" 1539 1540 msgid "Chat on WhatsApp" 1541 msgstr "الدردشة عبر WhatsApp" 1542 1543 msgid "about %s" 1544 msgstr "حول %s" 1545 1546 msgid "Ask about this product on WhatsApp" 1547 msgstr "اسأل عن هذا المنتج عبر WhatsApp" 1548 1549 msgid "WhatsApp Chat Button" 1550 msgstr "زر الدردشة على WhatsApp" 1551 1552 msgid "Add a free one-tap WhatsApp chat button to your store. It works with no GoUltra account: it opens a WhatsApp chat to your number, using a standard click-to-chat link." 1553 msgstr "أضف إلى متجرك زر دردشة WhatsApp مجاني بنقرة واحدة. يعمل بدون حساب GoUltra: يفتح دردشة WhatsApp إلى رقمك باستخدام رابط دردشة قياسي." 1554 1555 msgid "Show the floating chat button" 1556 msgstr "إظهار زر الدردشة العائم" 1557 1558 msgid "A round WhatsApp button in the corner of your storefront." 1559 msgstr "زر WhatsApp دائري في زاوية متجرك." 1560 1561 msgid "Include the country code. This is the number that will receive the chats." 1562 msgstr "أدرج رمز الدولة. هذا هو الرقم الذي سيستقبل الدردشات." 1563 1564 msgid "Pre-filled message" 1565 msgstr "رسالة معبأة مسبقاً" 1566 1567 msgid "Hi! I have a question about your store." 1568 msgstr "مرحباً! لدي سؤال حول متجرك." 1569 1570 msgid "The message the customer starts with. Leave empty for none." 1571 msgstr "الرسالة التي يبدأ بها العميل. اتركها فارغة لعدم استخدام أي رسالة." 1572 1573 msgid "Button label" 1574 msgstr "نص الزر" 1575 1576 msgid "Position" 1577 msgstr "الموضع" 1578 1579 msgid "Bottom right" 1580 msgstr "أسفل اليمين" 1581 1582 msgid "Bottom left" 1583 msgstr "أسفل اليسار" 1584 1585 msgid "Show on" 1586 msgstr "إظهار في" 1587 1588 msgid "All pages" 1589 msgstr "كل الصفحات" 1590 1591 msgid "Shop and product pages only" 1592 msgstr "صفحات المتجر والمنتجات فقط" 1593 1594 msgid "Add a button on product pages" 1595 msgstr "أضف زراً في صفحات المنتجات" 1596 1597 msgid "Shows an \"Ask about this product\" WhatsApp button under Add to cart, with the product name filled in." 1598 msgstr "يعرض زر WhatsApp \"اسأل عن هذا المنتج\" أسفل زر الإضافة إلى السلة، مع اسم المنتج." 1599 1600 msgid "Tip: you can also place a button anywhere with the shortcode" 1601 msgstr "نصيحة: يمكنك أيضاً وضع زر في أي مكان باستخدام الشورت كود" -
goultra-for-woocommerce/trunk/languages/goultra-for-woocommerce-es_ES.po
r3603449 r3617733 913 913 msgid "Send a test message (optional)" 914 914 msgstr "Enviar un mensaje de prueba (opcional)" 915 916 msgid "Campaigns" 917 msgstr "Campañas" 918 919 msgid "New campaign" 920 msgstr "Nueva campaña" 921 922 msgid "Compose a WhatsApp campaign for your Marketing List subscribers. Unsubscribed contacts are always filtered out automatically." 923 msgstr "Crea una campaña de WhatsApp para los suscriptores de tu lista de marketing. Los contactos dados de baja siempre se filtran automáticamente." 924 925 msgid "Connect your store to GoUltra to send campaigns." 926 msgstr "Conecta tu tienda a GoUltra para enviar campañas." 927 928 msgid "Template" 929 msgstr "Plantilla" 930 931 msgid "Refresh" 932 msgstr "Actualizar" 933 934 msgid "No approved marketing templates yet. Create one below." 935 msgstr "Aún no hay plantillas de marketing aprobadas. Crea una abajo." 936 937 msgid "Variables" 938 msgstr "Variables" 939 940 msgid "Header image" 941 msgstr "Imagen de cabecera" 942 943 msgid "Media Library" 944 msgstr "Biblioteca de medios" 945 946 msgid "Pick a product" 947 msgstr "Elegir un producto" 948 949 msgid "Search products" 950 msgstr "Buscar productos" 951 952 msgid "Send a test to your phone first" 953 msgstr "Envía primero una prueba a tu teléfono" 954 955 msgid "Send test" 956 msgstr "Enviar prueba" 957 958 msgid "Test sent. Check your WhatsApp." 959 msgstr "Prueba enviada. Revisa tu WhatsApp." 960 961 msgid "Audience" 962 msgstr "Audiencia" 963 964 msgid "When to send" 965 msgstr "Cuándo enviar" 966 967 msgid "Send now" 968 msgstr "Enviar ahora" 969 970 msgid "Schedule for later" 971 msgstr "Programar para más tarde" 972 973 msgid "Launch campaign" 974 msgstr "Lanzar la campaña" 975 976 msgid "online" 977 msgstr "en línea" 978 979 msgid "Choose a template to see the preview." 980 msgstr "Elige una plantilla para ver la vista previa." 981 982 msgid "Campaign history" 983 msgstr "Historial de campañas" 984 985 msgid "Sent" 986 msgstr "Enviados" 987 988 msgid "Failed" 989 msgstr "Fallidos" 990 991 msgid "Create a marketing template" 992 msgstr "Crear una plantilla de marketing" 993 994 msgid "Pick a language and style, adjust the text, and submit for Meta approval. A localized opt-out footer is added automatically, as Meta requires for marketing." 995 msgstr "Elige un idioma y un estilo, ajusta el texto y envíalo para la aprobación de Meta. Se añade automáticamente un pie de baja localizado, como exige Meta para marketing." 996 997 msgid "Language" 998 msgstr "Idioma" 999 1000 msgid "Style" 1001 msgstr "Estilo" 1002 1003 msgid "Message text" 1004 msgstr "Texto del mensaje" 1005 1006 msgid "Button label (optional)" 1007 msgstr "Etiqueta del botón (opcional)" 1008 1009 msgid "Button link (optional)" 1010 msgstr "Enlace del botón (opcional)" 1011 1012 msgid "Submit for approval" 1013 msgstr "Enviar para aprobación" 1014 1015 msgid "Something went wrong. Please try again." 1016 msgstr "Algo salió mal. Inténtalo de nuevo." 1017 1018 msgid "Choose a template" 1019 msgstr "Elige una plantilla" 1020 1021 msgid "Choose a template first." 1022 msgstr "Elige primero una plantilla." 1023 1024 msgid "Awaiting Meta approval:" 1025 msgstr "Esperando la aprobación de Meta:" 1026 1027 msgid "Customer first name" 1028 msgstr "Nombre del cliente" 1029 1030 msgid "Customer full name" 1031 msgstr "Nombre completo del cliente" 1032 1033 msgid "Fixed value" 1034 msgstr "Valor fijo" 1035 1036 msgid "No products with a usable image were found." 1037 msgstr "No se encontraron productos con una imagen utilizable." 1038 1039 msgid "Sending..." 1040 msgstr "Enviando..." 1041 1042 msgid "Please fill in all template variables." 1043 msgstr "Rellena todas las variables de la plantilla." 1044 1045 msgid "This template needs a header image. Choose one from the media library, a product, or paste an https image link." 1046 msgstr "Esta plantilla necesita una imagen de cabecera. Elige una de la biblioteca de medios, de un producto o pega un enlace https." 1047 1048 msgid "%d subscribers will receive this campaign." 1049 msgstr "%d suscriptores recibirán esta campaña." 1050 1051 msgid "Send this campaign to %d subscribers now?" 1052 msgstr "¿Enviar esta campaña a %d suscriptores ahora?" 1053 1054 msgid "Schedule this campaign to %d subscribers?" 1055 msgstr "¿Programar esta campaña para %d suscriptores?" 1056 1057 msgid "Campaign launched. Messages are on their way." 1058 msgstr "Campaña lanzada. Los mensajes están en camino." 1059 1060 msgid "Campaign scheduled." 1061 msgstr "Campaña programada." 1062 1063 msgid "No campaigns yet." 1064 msgstr "Aún no hay campañas." 1065 1066 msgid "Pause" 1067 msgstr "Pausar" 1068 1069 msgid "Cancel" 1070 msgstr "Cancelar" 1071 1072 msgid "Pause this campaign?" 1073 msgstr "¿Pausar esta campaña?" 1074 1075 msgid "Cancel this scheduled campaign?" 1076 msgstr "¿Cancelar esta campaña programada?" 1077 1078 msgid "Processing" 1079 msgstr "En proceso" 1080 1081 msgid "Scheduled" 1082 msgstr "Programada" 1083 1084 msgid "Completed" 1085 msgstr "Completada" 1086 1087 msgid "Cancelled" 1088 msgstr "Cancelada" 1089 1090 msgid "Paused" 1091 msgstr "En pausa" 1092 1093 msgid "Opt-out footer (added automatically):" 1094 msgstr "Pie de baja (añadido automáticamente):" 1095 1096 msgid "The message text must be between 15 and 1024 characters." 1097 msgstr "El texto del mensaje debe tener entre 15 y 1024 caracteres." 1098 1099 msgid "Template submitted. Meta approval usually takes minutes to a few hours." 1100 msgstr "Plantilla enviada. La aprobación de Meta suele tardar de minutos a unas horas." 1101 1102 msgid "A button needs both a label and a link." 1103 msgstr "Un botón necesita una etiqueta y un enlace." 1104 1105 msgid "Could not submit the template. Please try again." 1106 msgstr "No se pudo enviar la plantilla. Inténtalo de nuevo." 1107 1108 msgid "Could not load templates. Please try again." 1109 msgstr "No se pudieron cargar las plantillas. Inténtalo de nuevo." 1110 1111 msgid "Test send failed. Please try again." 1112 msgstr "El envío de prueba falló. Inténtalo de nuevo." 1113 1114 msgid "Could not launch the campaign. Please try again." 1115 msgstr "No se pudo lanzar la campaña. Inténtalo de nuevo." 1116 1117 msgid "The action failed. Please try again." 1118 msgstr "La acción falló. Inténtalo de nuevo." 1119 1120 msgid "Invalid request." 1121 msgstr "Solicitud no válida." 1122 1123 msgid "Choose a valid date and time for the scheduled send." 1124 msgstr "Elige una fecha y hora válidas para el envío programado." 1125 1126 msgid "A campaign was just launched. Please wait two minutes before launching another." 1127 msgstr "Se acaba de lanzar una campaña. Espera dos minutos antes de lanzar otra." 1128 1129 msgid "Your audience is larger than 5,000 contacts. Please run this campaign from the GoUltra dashboard." 1130 msgstr "Tu audiencia supera los 5.000 contactos. Ejecuta esta campaña desde el panel de GoUltra." 1131 1132 msgid "Your GoUltra account has more than one WhatsApp number connected, so the sending number must be chosen in the GoUltra dashboard for this store." 1133 msgstr "Tu cuenta de GoUltra tiene más de un número de WhatsApp conectado, así que el número de envío de esta tienda debe elegirse en el panel de GoUltra." 1134 1135 msgid "Your GoUltra plan message allowance for this cycle has been reached. Upgrade your plan to send more." 1136 msgstr "Se alcanzó el límite de mensajes de tu plan de GoUltra para este ciclo. Mejora tu plan para enviar más." 1137 1138 msgid "GoUltra for WooCommerce - WhatsApp Marketing, Notifications & Abandoned Cart" 1139 msgstr "GoUltra para WooCommerce - Marketing, notificaciones y carrito abandonado por WhatsApp" 1140 1141 msgid "WooCommerce WhatsApp notifications: automated order, shipping and abandoned-cart messages, plus opt-in marketing campaigns, powered by GoUltra." 1142 msgstr "Notificaciones de WhatsApp para WooCommerce: mensajes automáticos de pedido, envío y carrito abandonado, más campañas de marketing con consentimiento, con GoUltra." 1143 1144 msgid "Marketing Offer" 1145 msgstr "Oferta de marketing" 1146 1147 msgid "Promotions and offers for opted-in subscribers" 1148 msgstr "Promociones y ofertas para suscriptores que optaron por participar" 1149 1150 msgid "Recipients" 1151 msgstr "Destinatarios" 1152 1153 msgid "All subscribers" 1154 msgstr "Todos los suscriptores" 1155 1156 msgid "Choose specific contacts" 1157 msgstr "Elegir contactos especificos" 1158 1159 msgid "Search name or number" 1160 msgstr "Buscar nombre o numero" 1161 1162 msgid "Select all" 1163 msgstr "Seleccionar todo" 1164 1165 msgid "Clear" 1166 msgstr "Borrar" 1167 1168 msgid "Also send to my own number (%s) for a test" 1169 msgstr "Enviar tambien a mi propio numero (%s) como prueba" 1170 1171 msgid "Also send to my own number for a test (set your admin number in Settings)" 1172 msgstr "Enviar tambien a mi propio numero como prueba (configura tu numero de administrador en Ajustes)" 1173 1174 msgid "Need a marketing template?" 1175 msgstr "¿Necesitas una plantilla de marketing?" 1176 1177 msgid "Create one in the Templates tab (Marketing Offer): pick a language and ready-made style, and the opt-out footer is added automatically. Approved templates appear in the picker above." 1178 msgstr "Creala en la pestana Plantillas (Oferta de marketing): elige un idioma y un estilo listo, y el pie de baja se anade automaticamente. Las plantillas aprobadas aparecen en el selector de arriba." 1179 1180 msgid "Open Templates" 1181 msgstr "Abrir plantillas" 1182 1183 msgid "Delivery receipts and product sync" 1184 msgstr "Recibos de entrega y sincronizacion de productos" 1185 1186 msgid "These work automatically once your store is connected. There is nothing to set up." 1187 msgstr "Esto funciona automaticamente una vez que tu tienda esta conectada. No hay nada que configurar." 1188 1189 msgid "Connected and active." 1190 msgstr "Conectado y activo." 1191 1192 msgid "Message delivery receipts appear as order notes, and your product catalog is available to GoUltra automatically. No secret or URL to copy anywhere." 1193 msgstr "Los recibos de entrega de mensajes aparecen como notas del pedido y tu catalogo de productos esta disponible para GoUltra automaticamente. No hay ningun secreto ni URL que copiar." 1194 1195 msgid "Technical details (for support)" 1196 msgstr "Detalles tecnicos (para soporte)" 1197 1198 msgid "Webhook secret" 1199 msgstr "Secreto de webhook" 1200 1201 msgid "The plugin shares the secret with GoUltra automatically on connect, so you never paste it anywhere." 1202 msgstr "El plugin comparte el secreto con GoUltra automaticamente al conectar, asi que nunca tienes que pegarlo en ningun sitio." 1203 1204 msgid "All subscribers (%d)" 1205 msgstr "Todos los suscriptores (%d)" 1206 1207 msgid "%d selected" 1208 msgstr "%d seleccionados" 1209 1210 msgid "No subscribers yet." 1211 msgstr "Aun no hay suscriptores." 1212 1213 msgid "No recipients selected. Choose subscribers, or add your own number to send yourself a test." 1214 msgstr "No hay destinatarios seleccionados. Elige suscriptores o anade tu propio numero para enviarte una prueba." 1215 1216 msgid "%d recipients will receive this campaign." 1217 msgstr "%d destinatarios recibiran esta campana." 1218 1219 msgid "Send this campaign to %d recipients now?" 1220 msgstr "¿Enviar esta campana a %d destinatarios ahora?" 1221 1222 msgid "Schedule this campaign to %d recipients?" 1223 msgstr "¿Programar esta campana para %d destinatarios?" 1224 1225 msgid "Add my number" 1226 msgstr "Anadir mi numero" 1227 1228 msgid "Add to the list" 1229 msgstr "Anadir a la lista" 1230 1231 msgid "Adding..." 1232 msgstr "Anadiendo..." 1233 1234 msgid "Added to the list." 1235 msgstr "Anadido a la lista." 1236 1237 msgid "Could not add the contact. Please try again." 1238 msgstr "No se pudo anadir el contacto. Intentalo de nuevo." 1239 1240 msgid "Adds the number to the marketing list as a subscribed contact. Useful for sending yourself a test campaign: on the Campaigns screen choose \"Choose specific contacts\" and select only your number." 1241 msgstr "Anade el numero a la lista de marketing como contacto suscrito. Util para enviarte una campana de prueba: en la pantalla Campanas elige \"Elegir contactos especificos\" y selecciona solo tu numero." 1242 1243 msgid "Tip: to test on yourself, add your own number on the Marketing List screen, then select only it here." 1244 msgstr "Consejo: para probar contigo mismo, anade tu numero en la pantalla Lista de marketing y luego seleccionalo solo aqui." 1245 1246 msgid "No recipients selected. Choose contacts from the list." 1247 msgstr "No hay destinatarios seleccionados. Elige contactos de la lista." 1248 1249 msgid "No name" 1250 msgstr "Sin nombre" 1251 1252 msgid "Today" 1253 msgstr "Hoy" 1254 1255 msgid "Your GoUltra account is currently on hold, so messages cannot be sent. Please contact GoUltra support to resume." 1256 msgstr "Tu cuenta de GoUltra esta suspendida temporalmente, por lo que no se pueden enviar mensajes. Contacta con el soporte de GoUltra para reanudar." 1257 1258 msgid "Edit name" 1259 msgstr "Editar nombre" 1260 1261 msgid "Schedule campaign" 1262 msgstr "Programar campana" 1263 1264 msgid "Will be sent on %1$s at %2$s (your store time)." 1265 msgstr "Se enviara el %1$s a las %2$s (hora de tu tienda)." 1266 1267 msgid "Could not update the name. Please try again." 1268 msgstr "No se pudo actualizar el nombre. Intentalo de nuevo." 1269 1270 msgid "GoUltra returned HTTP %d." 1271 msgstr "GoUltra devolvio HTTP %d." 1272 1273 msgid "Template name" 1274 msgstr "Nombre de la plantilla" 1275 1276 msgid "Lowercase letters, numbers and underscores only." 1277 msgstr "Solo letras minusculas, numeros y guiones bajos." 1278 1279 msgid "Marketing (offers, promotions)" 1280 msgstr "Marketing (ofertas, promociones)" 1281 1282 msgid "Utility (order, account updates)" 1283 msgstr "Utilidad (pedido, actualizaciones de cuenta)" 1284 1285 msgid "No header" 1286 msgstr "Sin cabecera" 1287 1288 msgid "Text" 1289 msgstr "Texto" 1290 1291 msgid "Video" 1292 msgstr "Video" 1293 1294 msgid "Document" 1295 msgstr "Documento" 1296 1297 msgid "Header text" 1298 msgstr "Texto de la cabecera" 1299 1300 msgid "You will choose the actual image/video/file when you send a campaign." 1301 msgstr "Elegiras la imagen/video/archivo real al enviar una campana." 1302 1303 msgid "Note:" 1304 msgstr "Nota:" 1305 1306 msgid "Utility templates are for transactional messages only (order, shipping, account). Promotional content will be rejected by Meta." 1307 msgstr "Las plantillas de utilidad son solo para mensajes transaccionales (pedido, envio, cuenta). Meta rechazara el contenido promocional." 1308 1309 msgid "Insert a variable" 1310 msgstr "Insertar una variable" 1311 1312 msgid "(filled in automatically at send time)" 1313 msgstr "(se rellena automaticamente al enviar)" 1314 1315 msgid "Buttons" 1316 msgstr "Botones" 1317 1318 msgid "(up to 10, optional)" 1319 msgstr "(hasta 10, opcional)" 1320 1321 msgid "Add button" 1322 msgstr "Anadir boton" 1323 1324 msgid "Opt-out method (required for marketing)" 1325 msgstr "Metodo de baja (obligatorio para marketing)" 1326 1327 msgid "Footer text (recommended)" 1328 msgstr "Texto de pie (recomendado)" 1329 1330 msgid "Unsubscribe button" 1331 msgstr "Boton de baja" 1332 1333 msgid "Footer" 1334 msgstr "Pie" 1335 1336 msgid "(small gray line, optional)" 1337 msgstr "(pequena linea gris, opcional)" 1338 1339 msgid "Quick reply" 1340 msgstr "Respuesta rapida" 1341 1342 msgid "URL link" 1343 msgstr "Enlace URL" 1344 1345 msgid "Phone number" 1346 msgstr "Numero de telefono" 1347 1348 msgid "Button text" 1349 msgstr "Texto del boton" 1350 1351 msgid "A URL or phone button needs a value." 1352 msgstr "Un boton de URL o telefono necesita un valor." 1353 1354 msgid "Remove" 1355 msgstr "Quitar" 1356 1357 msgid "Two variables cannot be next to each other. Add some text between them (Meta rejects it otherwise)." 1358 msgstr "Dos variables no pueden estar juntas. Anade texto entre ellas (Meta lo rechaza)." 1359 1360 msgid "The message text must be between 5 and 1024 characters." 1361 msgstr "El texto del mensaje debe tener entre 5 y 1024 caracteres." 1362 1363 msgid "Meta rejected the template. %1$s (fallback: %2$s)" 1364 msgstr "Meta rechazo la plantilla. %1$s (alternativa: %2$s)" 1365 1366 msgid "Unknown error." 1367 msgstr "Error desconocido." 1368 1369 msgid "The template could not be submitted. %s" 1370 msgstr "No se pudo enviar la plantilla. %s" 1371 1372 msgid "This number is not on your marketing list, so there is no name to update." 1373 msgstr "Este numero no esta en tu lista de marketing, por lo que no hay nombre que actualizar." 1374 1375 msgid "Paste your API key (it starts with gk_) from the GoUltra Dashboard → Settings → API." 1376 msgstr "Pega tu clave API (empieza por gk_) desde el panel de GoUltra → Ajustes → API." 1377 1378 msgid "Campaign name (optional)" 1379 msgstr "Nombre de la campaña (opcional)" 1380 1381 msgid "e.g. Summer Sale - July" 1382 msgstr "p. ej. Rebajas de verano - julio" 1383 1384 msgid "A name to recognize this campaign in your history." 1385 msgstr "Un nombre para reconocer esta campaña en tu historial." 1386 1387 msgid "Upload / Media Library" 1388 msgstr "Subir / Biblioteca de medios" 1389 1390 msgid "Image selected" 1391 msgstr "Imagen seleccionada" 1392 1393 msgid "Or paste an image URL" 1394 msgstr "O pega la URL de una imagen" 1395 1396 msgid "JPEG or PNG images only. Choose from your Media Library, pick a store product, or paste a URL." 1397 msgstr "Solo imágenes JPEG o PNG. Elige de tu biblioteca de medios, un producto de la tienda o pega una URL." 1398 1399 msgid "Enter a valid WhatsApp number, including the country code." 1400 msgstr "Introduce un número de WhatsApp válido, incluido el código de país." 1401 1402 msgid "Too many code requests. Please wait a while before trying again." 1403 msgstr "Demasiadas solicitudes de código. Espera un poco antes de volver a intentarlo." 1404 1405 msgid "Your WhatsApp verification template is still being approved by Meta (usually a few minutes, sometimes longer). Please try again shortly - nothing else is needed from you." 1406 msgstr "Tu plantilla de verificación de WhatsApp aún está siendo aprobada por Meta (normalmente unos minutos, a veces más). Inténtalo de nuevo en breve - no necesitas hacer nada más." 1407 1408 msgid "Could not send the code. Please check the number and try again." 1409 msgstr "No se pudo enviar el código. Comprueba el número e inténtalo de nuevo." 1410 1411 msgid "We sent a 6-digit code to %s on WhatsApp. Enter it below." 1412 msgstr "Enviamos un código de 6 dígitos a %s por WhatsApp. Introdúcelo abajo." 1413 1414 msgid "The code expired. Please request a new one." 1415 msgstr "El código caducó. Solicita uno nuevo." 1416 1417 msgid "Too many wrong attempts. Please request a new code." 1418 msgstr "Demasiados intentos incorrectos. Solicita un código nuevo." 1419 1420 msgid "Incorrect code. Attempts left: %d." 1421 msgstr "Código incorrecto. Intentos restantes: %d." 1422 1423 msgid "Your number is verified. It is now your store manager number and can be selected as a campaign recipient for your own tests." 1424 msgstr "Tu número está verificado. Ahora es tu número de administrador de la tienda y puedes seleccionarlo como destinatario de campaña para tus propias pruebas." 1425 1426 msgid "Store manager" 1427 msgstr "Administrador de la tienda" 1428 1429 msgid "Verify your number (recommended)" 1430 msgstr "Verifica tu número (recomendado)" 1431 1432 msgid "Confirm your WhatsApp number with a code. A verified number becomes your store manager number, which you can select as a campaign recipient to test your own campaigns." 1433 msgstr "Confirma tu número de WhatsApp con un código. Un número verificado se convierte en tu número de administrador de la tienda, que puedes seleccionar como destinatario de campaña para probar tus campañas." 1434 1435 msgid "Verified" 1436 msgstr "Verificado" 1437 1438 msgid "Verify a different number" 1439 msgstr "Verificar otro número" 1440 1441 msgid "Send code" 1442 msgstr "Enviar código" 1443 1444 msgid "Verify" 1445 msgstr "Verificar" 1446 1447 msgid "Resend code" 1448 msgstr "Reenviar código" 1449 1450 msgid "Verifying..." 1451 msgstr "Verificando..." 1452 1453 msgid "Enter your WhatsApp number first." 1454 msgstr "Introduce primero tu número de WhatsApp." 1455 1456 msgid "Enter the code you received." 1457 msgstr "Introduce el código que recibiste." 1458 1459 msgid "Could not send the code." 1460 msgstr "No se pudo enviar el código." 1461 1462 msgid "Incorrect code." 1463 msgstr "Código incorrecto." 1464 1465 msgid "Network error. Please try again." 1466 msgstr "Error de red. Inténtalo de nuevo." 1467 1468 msgid "Building a template for a product?" 1469 msgstr "¿Creas una plantilla para un producto?" 1470 1471 msgid "Pick a store product and we set up the template for it: an image header plus a \"View product\" button that links to it. Then choose a language and submit for approval." 1472 msgstr "Elige un producto de la tienda y preparamos la plantilla para él: una cabecera de imagen y un botón \"Ver producto\" que enlaza a él. Luego elige un idioma y envíala para aprobación." 1473 1474 msgid "View product" 1475 msgstr "Ver producto" 1476 1477 msgid "No products found." 1478 msgstr "No se encontraron productos." 1479 1480 msgid "You already have an approved or pending template of this type for this language. Create another one?" 1481 msgstr "Ya tienes una plantilla aprobada o pendiente de este tipo para este idioma. ¿Crear otra?" 1482 1483 msgid "Unconfirmed - verify before resending" 1484 msgstr "Sin confirmar - verifica antes de reenviar" 1485 1486 msgid "The connection dropped before we got a reply, so the campaign may or may not have gone out. Check Campaign History and your GoUltra dashboard before launching it again to avoid sending it twice." 1487 msgstr "La conexion se corto antes de recibir respuesta, asi que la campana pudo haberse enviado o no. Revisa el historial de campanas y tu panel de GoUltra antes de lanzarla de nuevo para no enviarla dos veces." 1488 1489 msgid "GoUltra for WooCommerce: connection problem." 1490 msgstr "GoUltra para WooCommerce: problema de conexion." 1491 1492 msgid "Recent WhatsApp messages were rejected because the API key is no longer valid (it may have been regenerated or the account changed). Messages are not being delivered." 1493 msgstr "Los mensajes de WhatsApp recientes fueron rechazados porque la clave API ya no es valida (puede haberse regenerado o la cuenta cambio). Los mensajes no se estan entregando." 1494 1495 msgid "Reconnect your store" 1496 msgstr "Vuelve a conectar tu tienda" 1497 1498 msgid "Image" 1499 msgstr "Imagen" 1500 1501 msgid "View" 1502 msgstr "Ver" 1503 1504 msgid "rejected" 1505 msgstr "rechazada" 1506 1507 msgid "No templates match your search." 1508 msgstr "Ninguna plantilla coincide con tu busqueda." 1509 1510 msgid "Search your templates by name, text or language..." 1511 msgstr "Busca tus plantillas por nombre, texto o idioma..." 1512 1513 msgid "Close" 1514 msgstr "Cerrar" 1515 1516 msgid "GoUltra did not accept this store's key for this action. Your connection is fine (order messages work), but campaign sending for this number may not be enabled yet on your GoUltra account. Please contact GoUltra support to enable it." 1517 msgstr "GoUltra no acepto la clave de esta tienda para esta accion. Tu conexion esta bien (los mensajes de pedido se envian), pero el envio de campanas para este numero puede no estar habilitado aun en tu cuenta de GoUltra. Contacta con el soporte de GoUltra para habilitarlo." 1518 1519 msgid "JPEG or PNG images only." 1520 msgstr "Solo imagenes JPEG o PNG." 1521 1522 msgid "Marketing sending is turned off for this WhatsApp number. Enable marketing for this number in your GoUltra dashboard to send campaigns." 1523 msgstr "El envio de marketing esta desactivado para este numero de WhatsApp. Activa el marketing para este numero en tu panel de GoUltra para enviar campanas." 1524 1525 msgid "Chat with us" 1526 msgstr "Chatea con nosotros" 1527 1528 msgid "Chat on WhatsApp" 1529 msgstr "Chatea por WhatsApp" 1530 1531 msgid "about %s" 1532 msgstr "sobre %s" 1533 1534 msgid "Ask about this product on WhatsApp" 1535 msgstr "Pregunta por este producto en WhatsApp" 1536 1537 msgid "WhatsApp Chat Button" 1538 msgstr "Botón de chat de WhatsApp" 1539 1540 msgid "Add a free one-tap WhatsApp chat button to your store. It works with no GoUltra account: it opens a WhatsApp chat to your number, using a standard click-to-chat link." 1541 msgstr "Añade a tu tienda un botón gratuito de chat de WhatsApp con un toque. Funciona sin cuenta de GoUltra: abre un chat de WhatsApp a tu número mediante un enlace de chat estándar." 1542 1543 msgid "Show the floating chat button" 1544 msgstr "Mostrar el botón de chat flotante" 1545 1546 msgid "A round WhatsApp button in the corner of your storefront." 1547 msgstr "Un botón redondo de WhatsApp en la esquina de tu tienda." 1548 1549 msgid "Include the country code. This is the number that will receive the chats." 1550 msgstr "Incluye el código de país. Este es el número que recibirá los chats." 1551 1552 msgid "Pre-filled message" 1553 msgstr "Mensaje predefinido" 1554 1555 msgid "Hi! I have a question about your store." 1556 msgstr "¡Hola! Tengo una pregunta sobre tu tienda." 1557 1558 msgid "The message the customer starts with. Leave empty for none." 1559 msgstr "El mensaje con el que empieza el cliente. Déjalo vacío para ninguno." 1560 1561 msgid "Button label" 1562 msgstr "Etiqueta del botón" 1563 1564 msgid "Position" 1565 msgstr "Posición" 1566 1567 msgid "Bottom right" 1568 msgstr "Abajo a la derecha" 1569 1570 msgid "Bottom left" 1571 msgstr "Abajo a la izquierda" 1572 1573 msgid "Show on" 1574 msgstr "Mostrar en" 1575 1576 msgid "All pages" 1577 msgstr "Todas las páginas" 1578 1579 msgid "Shop and product pages only" 1580 msgstr "Solo páginas de tienda y producto" 1581 1582 msgid "Add a button on product pages" 1583 msgstr "Añadir un botón en las páginas de producto" 1584 1585 msgid "Shows an \"Ask about this product\" WhatsApp button under Add to cart, with the product name filled in." 1586 msgstr "Muestra un botón de WhatsApp \"Pregunta por este producto\" debajo de Añadir al carrito, con el nombre del producto." 1587 1588 msgid "Tip: you can also place a button anywhere with the shortcode" 1589 msgstr "Consejo: también puedes colocar un botón en cualquier lugar con el shortcode" -
goultra-for-woocommerce/trunk/languages/goultra-for-woocommerce-fr_FR.po
r3603449 r3617733 913 913 msgid "Send a test message (optional)" 914 914 msgstr "Envoyer un message de test (facultatif)" 915 916 msgid "Campaigns" 917 msgstr "Campagnes" 918 919 msgid "New campaign" 920 msgstr "Nouvelle campagne" 921 922 msgid "Compose a WhatsApp campaign for your Marketing List subscribers. Unsubscribed contacts are always filtered out automatically." 923 msgstr "Composez une campagne WhatsApp pour les abonnés de votre liste marketing. Les contacts désabonnés sont toujours filtrés automatiquement." 924 925 msgid "Connect your store to GoUltra to send campaigns." 926 msgstr "Connectez votre boutique à GoUltra pour envoyer des campagnes." 927 928 msgid "Template" 929 msgstr "Modèle" 930 931 msgid "Refresh" 932 msgstr "Actualiser" 933 934 msgid "No approved marketing templates yet. Create one below." 935 msgstr "Aucun modèle marketing approuvé pour le moment. Créez-en un ci-dessous." 936 937 msgid "Variables" 938 msgstr "Variables" 939 940 msgid "Header image" 941 msgstr "Image d'en-tête" 942 943 msgid "Media Library" 944 msgstr "Médiathèque" 945 946 msgid "Pick a product" 947 msgstr "Choisir un produit" 948 949 msgid "Search products" 950 msgstr "Rechercher des produits" 951 952 msgid "Send a test to your phone first" 953 msgstr "Envoyez d'abord un test sur votre téléphone" 954 955 msgid "Send test" 956 msgstr "Envoyer un test" 957 958 msgid "Test sent. Check your WhatsApp." 959 msgstr "Test envoyé. Vérifiez votre WhatsApp." 960 961 msgid "Audience" 962 msgstr "Audience" 963 964 msgid "When to send" 965 msgstr "Quand envoyer" 966 967 msgid "Send now" 968 msgstr "Envoyer maintenant" 969 970 msgid "Schedule for later" 971 msgstr "Programmer pour plus tard" 972 973 msgid "Launch campaign" 974 msgstr "Lancer la campagne" 975 976 msgid "online" 977 msgstr "en ligne" 978 979 msgid "Choose a template to see the preview." 980 msgstr "Choisissez un modèle pour voir l'aperçu." 981 982 msgid "Campaign history" 983 msgstr "Historique des campagnes" 984 985 msgid "Sent" 986 msgstr "Envoyés" 987 988 msgid "Failed" 989 msgstr "Échoués" 990 991 msgid "Create a marketing template" 992 msgstr "Créer un modèle marketing" 993 994 msgid "Pick a language and style, adjust the text, and submit for Meta approval. A localized opt-out footer is added automatically, as Meta requires for marketing." 995 msgstr "Choisissez une langue et un style, ajustez le texte et soumettez-le à l'approbation de Meta. Un pied de désinscription localisé est ajouté automatiquement, comme Meta l'exige pour le marketing." 996 997 msgid "Language" 998 msgstr "Langue" 999 1000 msgid "Style" 1001 msgstr "Style" 1002 1003 msgid "Message text" 1004 msgstr "Texte du message" 1005 1006 msgid "Button label (optional)" 1007 msgstr "Libellé du bouton (facultatif)" 1008 1009 msgid "Button link (optional)" 1010 msgstr "Lien du bouton (facultatif)" 1011 1012 msgid "Submit for approval" 1013 msgstr "Soumettre pour approbation" 1014 1015 msgid "Something went wrong. Please try again." 1016 msgstr "Un problème est survenu. Veuillez réessayer." 1017 1018 msgid "Choose a template" 1019 msgstr "Choisissez un modèle" 1020 1021 msgid "Choose a template first." 1022 msgstr "Choisissez d'abord un modèle." 1023 1024 msgid "Awaiting Meta approval:" 1025 msgstr "En attente de l'approbation de Meta :" 1026 1027 msgid "Customer first name" 1028 msgstr "Prénom du client" 1029 1030 msgid "Customer full name" 1031 msgstr "Nom complet du client" 1032 1033 msgid "Fixed value" 1034 msgstr "Valeur fixe" 1035 1036 msgid "No products with a usable image were found." 1037 msgstr "Aucun produit avec une image utilisable n'a été trouvé." 1038 1039 msgid "Sending..." 1040 msgstr "Envoi..." 1041 1042 msgid "Please fill in all template variables." 1043 msgstr "Veuillez remplir toutes les variables du modèle." 1044 1045 msgid "This template needs a header image. Choose one from the media library, a product, or paste an https image link." 1046 msgstr "Ce modèle nécessite une image d'en-tête. Choisissez-en une dans la médiathèque, un produit, ou collez un lien https." 1047 1048 msgid "%d subscribers will receive this campaign." 1049 msgstr "%d abonnés recevront cette campagne." 1050 1051 msgid "Send this campaign to %d subscribers now?" 1052 msgstr "Envoyer cette campagne à %d abonnés maintenant ?" 1053 1054 msgid "Schedule this campaign to %d subscribers?" 1055 msgstr "Programmer cette campagne pour %d abonnés ?" 1056 1057 msgid "Campaign launched. Messages are on their way." 1058 msgstr "Campagne lancée. Les messages sont en route." 1059 1060 msgid "Campaign scheduled." 1061 msgstr "Campagne programmée." 1062 1063 msgid "No campaigns yet." 1064 msgstr "Aucune campagne pour le moment." 1065 1066 msgid "Pause" 1067 msgstr "Pause" 1068 1069 msgid "Cancel" 1070 msgstr "Annuler" 1071 1072 msgid "Pause this campaign?" 1073 msgstr "Mettre cette campagne en pause ?" 1074 1075 msgid "Cancel this scheduled campaign?" 1076 msgstr "Annuler cette campagne programmée ?" 1077 1078 msgid "Processing" 1079 msgstr "En cours" 1080 1081 msgid "Scheduled" 1082 msgstr "Programmée" 1083 1084 msgid "Completed" 1085 msgstr "Terminée" 1086 1087 msgid "Cancelled" 1088 msgstr "Annulée" 1089 1090 msgid "Paused" 1091 msgstr "En pause" 1092 1093 msgid "Opt-out footer (added automatically):" 1094 msgstr "Pied de désinscription (ajouté automatiquement) :" 1095 1096 msgid "The message text must be between 15 and 1024 characters." 1097 msgstr "Le texte du message doit comporter entre 15 et 1024 caractères." 1098 1099 msgid "Template submitted. Meta approval usually takes minutes to a few hours." 1100 msgstr "Modèle soumis. L'approbation de Meta prend généralement de quelques minutes à quelques heures." 1101 1102 msgid "A button needs both a label and a link." 1103 msgstr "Un bouton nécessite un libellé et un lien." 1104 1105 msgid "Could not submit the template. Please try again." 1106 msgstr "Impossible de soumettre le modèle. Veuillez réessayer." 1107 1108 msgid "Could not load templates. Please try again." 1109 msgstr "Impossible de charger les modèles. Veuillez réessayer." 1110 1111 msgid "Test send failed. Please try again." 1112 msgstr "L'envoi du test a échoué. Veuillez réessayer." 1113 1114 msgid "Could not launch the campaign. Please try again." 1115 msgstr "Impossible de lancer la campagne. Veuillez réessayer." 1116 1117 msgid "The action failed. Please try again." 1118 msgstr "L'action a échoué. Veuillez réessayer." 1119 1120 msgid "Invalid request." 1121 msgstr "Requête non valide." 1122 1123 msgid "Choose a valid date and time for the scheduled send." 1124 msgstr "Choisissez une date et une heure valides pour l'envoi programmé." 1125 1126 msgid "A campaign was just launched. Please wait two minutes before launching another." 1127 msgstr "Une campagne vient d'être lancée. Attendez deux minutes avant d'en lancer une autre." 1128 1129 msgid "Your audience is larger than 5,000 contacts. Please run this campaign from the GoUltra dashboard." 1130 msgstr "Votre audience dépasse 5 000 contacts. Lancez cette campagne depuis le tableau de bord GoUltra." 1131 1132 msgid "Your GoUltra account has more than one WhatsApp number connected, so the sending number must be chosen in the GoUltra dashboard for this store." 1133 msgstr "Votre compte GoUltra a plusieurs numéros WhatsApp connectés ; le numéro d'envoi de cette boutique doit être choisi dans le tableau de bord GoUltra." 1134 1135 msgid "Your GoUltra plan message allowance for this cycle has been reached. Upgrade your plan to send more." 1136 msgstr "Le quota de messages de votre forfait GoUltra pour ce cycle est atteint. Améliorez votre forfait pour en envoyer plus." 1137 1138 msgid "GoUltra for WooCommerce - WhatsApp Marketing, Notifications & Abandoned Cart" 1139 msgstr "GoUltra pour WooCommerce - Marketing, notifications et panier abandonné par WhatsApp" 1140 1141 msgid "WooCommerce WhatsApp notifications: automated order, shipping and abandoned-cart messages, plus opt-in marketing campaigns, powered by GoUltra." 1142 msgstr "Notifications WhatsApp pour WooCommerce : messages automatisés de commande, de livraison et de panier abandonné, plus des campagnes marketing avec consentement, avec GoUltra." 1143 1144 msgid "Marketing Offer" 1145 msgstr "Offre marketing" 1146 1147 msgid "Promotions and offers for opted-in subscribers" 1148 msgstr "Promotions et offres pour les abonnes inscrits" 1149 1150 msgid "Recipients" 1151 msgstr "Destinataires" 1152 1153 msgid "All subscribers" 1154 msgstr "Tous les abonnes" 1155 1156 msgid "Choose specific contacts" 1157 msgstr "Choisir des contacts specifiques" 1158 1159 msgid "Search name or number" 1160 msgstr "Rechercher un nom ou un numero" 1161 1162 msgid "Select all" 1163 msgstr "Tout selectionner" 1164 1165 msgid "Clear" 1166 msgstr "Effacer" 1167 1168 msgid "Also send to my own number (%s) for a test" 1169 msgstr "Envoyer aussi a mon propre numero (%s) pour un test" 1170 1171 msgid "Also send to my own number for a test (set your admin number in Settings)" 1172 msgstr "Envoyer aussi a mon propre numero pour un test (definissez votre numero admin dans les reglages)" 1173 1174 msgid "Need a marketing template?" 1175 msgstr "Besoin d'un modele marketing ?" 1176 1177 msgid "Create one in the Templates tab (Marketing Offer): pick a language and ready-made style, and the opt-out footer is added automatically. Approved templates appear in the picker above." 1178 msgstr "Creez-le dans l'onglet Modeles (Offre marketing) : choisissez une langue et un style pret, le pied de desinscription est ajoute automatiquement. Les modeles approuves apparaissent dans le selecteur ci-dessus." 1179 1180 msgid "Open Templates" 1181 msgstr "Ouvrir les modeles" 1182 1183 msgid "Delivery receipts and product sync" 1184 msgstr "Accuses de reception et synchronisation produits" 1185 1186 msgid "These work automatically once your store is connected. There is nothing to set up." 1187 msgstr "Cela fonctionne automatiquement une fois votre boutique connectee. Il n'y a rien a configurer." 1188 1189 msgid "Connected and active." 1190 msgstr "Connecte et actif." 1191 1192 msgid "Message delivery receipts appear as order notes, and your product catalog is available to GoUltra automatically. No secret or URL to copy anywhere." 1193 msgstr "Les accuses de reception des messages apparaissent comme des notes de commande, et votre catalogue produits est disponible pour GoUltra automatiquement. Aucun secret ni URL a copier." 1194 1195 msgid "Technical details (for support)" 1196 msgstr "Details techniques (pour le support)" 1197 1198 msgid "Webhook secret" 1199 msgstr "Secret du webhook" 1200 1201 msgid "The plugin shares the secret with GoUltra automatically on connect, so you never paste it anywhere." 1202 msgstr "L'extension partage le secret avec GoUltra automatiquement a la connexion, vous n'avez donc jamais a le coller nulle part." 1203 1204 msgid "All subscribers (%d)" 1205 msgstr "Tous les abonnes (%d)" 1206 1207 msgid "%d selected" 1208 msgstr "%d selectionnes" 1209 1210 msgid "No subscribers yet." 1211 msgstr "Aucun abonne pour l'instant." 1212 1213 msgid "No recipients selected. Choose subscribers, or add your own number to send yourself a test." 1214 msgstr "Aucun destinataire selectionne. Choisissez des abonnes, ou ajoutez votre propre numero pour vous envoyer un test." 1215 1216 msgid "%d recipients will receive this campaign." 1217 msgstr "%d destinataires recevront cette campagne." 1218 1219 msgid "Send this campaign to %d recipients now?" 1220 msgstr "Envoyer cette campagne a %d destinataires maintenant ?" 1221 1222 msgid "Schedule this campaign to %d recipients?" 1223 msgstr "Programmer cette campagne pour %d destinataires ?" 1224 1225 msgid "Add my number" 1226 msgstr "Ajouter mon numero" 1227 1228 msgid "Add to the list" 1229 msgstr "Ajouter a la liste" 1230 1231 msgid "Adding..." 1232 msgstr "Ajout..." 1233 1234 msgid "Added to the list." 1235 msgstr "Ajoute a la liste." 1236 1237 msgid "Could not add the contact. Please try again." 1238 msgstr "Impossible d'ajouter le contact. Veuillez reessayer." 1239 1240 msgid "Adds the number to the marketing list as a subscribed contact. Useful for sending yourself a test campaign: on the Campaigns screen choose \"Choose specific contacts\" and select only your number." 1241 msgstr "Ajoute le numero a la liste marketing comme contact abonne. Utile pour vous envoyer une campagne de test : sur l'ecran Campagnes, choisissez \"Choisir des contacts specifiques\" et selectionnez uniquement votre numero." 1242 1243 msgid "Tip: to test on yourself, add your own number on the Marketing List screen, then select only it here." 1244 msgstr "Astuce : pour tester sur vous-meme, ajoutez votre numero sur l'ecran Liste marketing, puis selectionnez-le uniquement ici." 1245 1246 msgid "No recipients selected. Choose contacts from the list." 1247 msgstr "Aucun destinataire selectionne. Choisissez des contacts dans la liste." 1248 1249 msgid "No name" 1250 msgstr "Sans nom" 1251 1252 msgid "Today" 1253 msgstr "Aujourd'hui" 1254 1255 msgid "Your GoUltra account is currently on hold, so messages cannot be sent. Please contact GoUltra support to resume." 1256 msgstr "Votre compte GoUltra est actuellement suspendu, les messages ne peuvent donc pas etre envoyes. Contactez le support GoUltra pour reprendre." 1257 1258 msgid "Edit name" 1259 msgstr "Modifier le nom" 1260 1261 msgid "Schedule campaign" 1262 msgstr "Programmer la campagne" 1263 1264 msgid "Will be sent on %1$s at %2$s (your store time)." 1265 msgstr "Sera envoye le %1$s a %2$s (heure de votre boutique)." 1266 1267 msgid "Could not update the name. Please try again." 1268 msgstr "Impossible de mettre a jour le nom. Veuillez reessayer." 1269 1270 msgid "GoUltra returned HTTP %d." 1271 msgstr "GoUltra a renvoye HTTP %d." 1272 1273 msgid "Template name" 1274 msgstr "Nom du modele" 1275 1276 msgid "Lowercase letters, numbers and underscores only." 1277 msgstr "Lettres minuscules, chiffres et traits de soulignement uniquement." 1278 1279 msgid "Marketing (offers, promotions)" 1280 msgstr "Marketing (offres, promotions)" 1281 1282 msgid "Utility (order, account updates)" 1283 msgstr "Utilitaire (commande, mises a jour du compte)" 1284 1285 msgid "No header" 1286 msgstr "Sans en-tete" 1287 1288 msgid "Text" 1289 msgstr "Texte" 1290 1291 msgid "Video" 1292 msgstr "Video" 1293 1294 msgid "Document" 1295 msgstr "Document" 1296 1297 msgid "Header text" 1298 msgstr "Texte de l'en-tete" 1299 1300 msgid "You will choose the actual image/video/file when you send a campaign." 1301 msgstr "Vous choisirez l'image/video/fichier reel lors de l'envoi d'une campagne." 1302 1303 msgid "Note:" 1304 msgstr "Remarque :" 1305 1306 msgid "Utility templates are for transactional messages only (order, shipping, account). Promotional content will be rejected by Meta." 1307 msgstr "Les modeles utilitaires sont reserves aux messages transactionnels (commande, expedition, compte). Meta rejettera tout contenu promotionnel." 1308 1309 msgid "Insert a variable" 1310 msgstr "Inserer une variable" 1311 1312 msgid "(filled in automatically at send time)" 1313 msgstr "(rempli automatiquement a l'envoi)" 1314 1315 msgid "Buttons" 1316 msgstr "Boutons" 1317 1318 msgid "(up to 10, optional)" 1319 msgstr "(jusqu'a 10, facultatif)" 1320 1321 msgid "Add button" 1322 msgstr "Ajouter un bouton" 1323 1324 msgid "Opt-out method (required for marketing)" 1325 msgstr "Methode de desinscription (obligatoire pour le marketing)" 1326 1327 msgid "Footer text (recommended)" 1328 msgstr "Texte de pied (recommande)" 1329 1330 msgid "Unsubscribe button" 1331 msgstr "Bouton de desinscription" 1332 1333 msgid "Footer" 1334 msgstr "Pied" 1335 1336 msgid "(small gray line, optional)" 1337 msgstr "(petite ligne grise, facultatif)" 1338 1339 msgid "Quick reply" 1340 msgstr "Reponse rapide" 1341 1342 msgid "URL link" 1343 msgstr "Lien URL" 1344 1345 msgid "Phone number" 1346 msgstr "Numero de telephone" 1347 1348 msgid "Button text" 1349 msgstr "Texte du bouton" 1350 1351 msgid "A URL or phone button needs a value." 1352 msgstr "Un bouton URL ou telephone necessite une valeur." 1353 1354 msgid "Remove" 1355 msgstr "Retirer" 1356 1357 msgid "Two variables cannot be next to each other. Add some text between them (Meta rejects it otherwise)." 1358 msgstr "Deux variables ne peuvent pas etre cote a cote. Ajoutez du texte entre elles (sinon Meta rejette)." 1359 1360 msgid "The message text must be between 5 and 1024 characters." 1361 msgstr "Le texte du message doit comporter entre 5 et 1024 caracteres." 1362 1363 msgid "Meta rejected the template. %1$s (fallback: %2$s)" 1364 msgstr "Meta a rejete le modele. %1$s (repli : %2$s)" 1365 1366 msgid "Unknown error." 1367 msgstr "Erreur inconnue." 1368 1369 msgid "The template could not be submitted. %s" 1370 msgstr "Le modele n'a pas pu etre soumis. %s" 1371 1372 msgid "This number is not on your marketing list, so there is no name to update." 1373 msgstr "Ce numero n'est pas dans votre liste marketing, il n'y a donc aucun nom a mettre a jour." 1374 1375 msgid "Paste your API key (it starts with gk_) from the GoUltra Dashboard → Settings → API." 1376 msgstr "Collez votre clé API (elle commence par gk_) depuis le tableau de bord GoUltra → Réglages → API." 1377 1378 msgid "Campaign name (optional)" 1379 msgstr "Nom de la campagne (facultatif)" 1380 1381 msgid "e.g. Summer Sale - July" 1382 msgstr "ex. Soldes d'ete - juillet" 1383 1384 msgid "A name to recognize this campaign in your history." 1385 msgstr "Un nom pour reconnaitre cette campagne dans votre historique." 1386 1387 msgid "Upload / Media Library" 1388 msgstr "Televerser / Mediatheque" 1389 1390 msgid "Image selected" 1391 msgstr "Image selectionnee" 1392 1393 msgid "Or paste an image URL" 1394 msgstr "Ou collez l'URL d'une image" 1395 1396 msgid "JPEG or PNG images only. Choose from your Media Library, pick a store product, or paste a URL." 1397 msgstr "Images JPEG ou PNG uniquement. Choisissez dans la mediatheque, selectionnez un produit ou collez une URL." 1398 1399 msgid "Enter a valid WhatsApp number, including the country code." 1400 msgstr "Saisissez un numéro WhatsApp valide, avec l'indicatif du pays." 1401 1402 msgid "Too many code requests. Please wait a while before trying again." 1403 msgstr "Trop de demandes de code. Patientez un moment avant de réessayer." 1404 1405 msgid "Your WhatsApp verification template is still being approved by Meta (usually a few minutes, sometimes longer). Please try again shortly - nothing else is needed from you." 1406 msgstr "Votre modèle de vérification WhatsApp est encore en cours d'approbation par Meta (généralement quelques minutes, parfois plus). Réessayez dans un instant - rien d'autre n'est requis de votre part." 1407 1408 msgid "Could not send the code. Please check the number and try again." 1409 msgstr "Impossible d'envoyer le code. Vérifiez le numéro et réessayez." 1410 1411 msgid "We sent a 6-digit code to %s on WhatsApp. Enter it below." 1412 msgstr "Nous avons envoyé un code à 6 chiffres au %s sur WhatsApp. Saisissez-le ci-dessous." 1413 1414 msgid "The code expired. Please request a new one." 1415 msgstr "Le code a expiré. Demandez-en un nouveau." 1416 1417 msgid "Too many wrong attempts. Please request a new code." 1418 msgstr "Trop de tentatives incorrectes. Demandez un nouveau code." 1419 1420 msgid "Incorrect code. Attempts left: %d." 1421 msgstr "Code incorrect. Tentatives restantes : %d." 1422 1423 msgid "Your number is verified. It is now your store manager number and can be selected as a campaign recipient for your own tests." 1424 msgstr "Votre numéro est vérifié. C'est désormais votre numéro de responsable de la boutique, sélectionnable comme destinataire de campagne pour vos propres tests." 1425 1426 msgid "Store manager" 1427 msgstr "Responsable de la boutique" 1428 1429 msgid "Verify your number (recommended)" 1430 msgstr "Vérifiez votre numéro (recommandé)" 1431 1432 msgid "Confirm your WhatsApp number with a code. A verified number becomes your store manager number, which you can select as a campaign recipient to test your own campaigns." 1433 msgstr "Confirmez votre numéro WhatsApp avec un code. Un numéro vérifié devient votre numéro de responsable de la boutique, sélectionnable comme destinataire de campagne pour tester vos campagnes." 1434 1435 msgid "Verified" 1436 msgstr "Vérifié" 1437 1438 msgid "Verify a different number" 1439 msgstr "Vérifier un autre numéro" 1440 1441 msgid "Send code" 1442 msgstr "Envoyer le code" 1443 1444 msgid "Verify" 1445 msgstr "Vérifier" 1446 1447 msgid "Resend code" 1448 msgstr "Renvoyer le code" 1449 1450 msgid "Verifying..." 1451 msgstr "Vérification..." 1452 1453 msgid "Enter your WhatsApp number first." 1454 msgstr "Saisissez d'abord votre numéro WhatsApp." 1455 1456 msgid "Enter the code you received." 1457 msgstr "Saisissez le code que vous avez reçu." 1458 1459 msgid "Could not send the code." 1460 msgstr "Impossible d'envoyer le code." 1461 1462 msgid "Incorrect code." 1463 msgstr "Code incorrect." 1464 1465 msgid "Network error. Please try again." 1466 msgstr "Erreur réseau. Réessayez." 1467 1468 msgid "Building a template for a product?" 1469 msgstr "Vous créez un modèle pour un produit ?" 1470 1471 msgid "Pick a store product and we set up the template for it: an image header plus a \"View product\" button that links to it. Then choose a language and submit for approval." 1472 msgstr "Choisissez un produit de la boutique et nous préparons le modèle : un en-tête image et un bouton \"Voir le produit\" qui y renvoie. Choisissez ensuite une langue et soumettez pour approbation." 1473 1474 msgid "View product" 1475 msgstr "Voir le produit" 1476 1477 msgid "No products found." 1478 msgstr "Aucun produit trouvé." 1479 1480 msgid "You already have an approved or pending template of this type for this language. Create another one?" 1481 msgstr "Vous avez déjà un modèle approuvé ou en attente de ce type pour cette langue. En créer un autre ?" 1482 1483 msgid "Unconfirmed - verify before resending" 1484 msgstr "Non confirme - verifiez avant de renvoyer" 1485 1486 msgid "The connection dropped before we got a reply, so the campaign may or may not have gone out. Check Campaign History and your GoUltra dashboard before launching it again to avoid sending it twice." 1487 msgstr "La connexion a ete interrompue avant la reponse, donc la campagne a peut-etre ete envoyee ou non. Verifiez l'historique des campagnes et votre tableau de bord GoUltra avant de la relancer pour eviter de l'envoyer deux fois." 1488 1489 msgid "GoUltra for WooCommerce: connection problem." 1490 msgstr "GoUltra pour WooCommerce : probleme de connexion." 1491 1492 msgid "Recent WhatsApp messages were rejected because the API key is no longer valid (it may have been regenerated or the account changed). Messages are not being delivered." 1493 msgstr "Les messages WhatsApp recents ont ete rejetes car la cle API n'est plus valide (elle a peut-etre ete regeneree ou le compte a change). Les messages ne sont pas delivres." 1494 1495 msgid "Reconnect your store" 1496 msgstr "Reconnectez votre boutique" 1497 1498 msgid "Image" 1499 msgstr "Image" 1500 1501 msgid "View" 1502 msgstr "Voir" 1503 1504 msgid "rejected" 1505 msgstr "refusee" 1506 1507 msgid "No templates match your search." 1508 msgstr "Aucun modele ne correspond a votre recherche." 1509 1510 msgid "Search your templates by name, text or language..." 1511 msgstr "Recherchez vos modeles par nom, texte ou langue..." 1512 1513 msgid "Close" 1514 msgstr "Fermer" 1515 1516 msgid "GoUltra did not accept this store's key for this action. Your connection is fine (order messages work), but campaign sending for this number may not be enabled yet on your GoUltra account. Please contact GoUltra support to enable it." 1517 msgstr "GoUltra n'a pas accepte la cle de cette boutique pour cette action. Votre connexion fonctionne (les messages de commande partent), mais l'envoi de campagnes pour ce numero n'est peut-etre pas encore active sur votre compte GoUltra. Contactez le support GoUltra pour l'activer." 1518 1519 msgid "JPEG or PNG images only." 1520 msgstr "Images JPEG ou PNG uniquement." 1521 1522 msgid "Marketing sending is turned off for this WhatsApp number. Enable marketing for this number in your GoUltra dashboard to send campaigns." 1523 msgstr "L'envoi marketing est desactive pour ce numero WhatsApp. Activez le marketing pour ce numero dans votre tableau de bord GoUltra pour envoyer des campagnes." 1524 1525 msgid "Chat with us" 1526 msgstr "Discutez avec nous" 1527 1528 msgid "Chat on WhatsApp" 1529 msgstr "Discuter sur WhatsApp" 1530 1531 msgid "about %s" 1532 msgstr "à propos de %s" 1533 1534 msgid "Ask about this product on WhatsApp" 1535 msgstr "Poser une question sur ce produit sur WhatsApp" 1536 1537 msgid "WhatsApp Chat Button" 1538 msgstr "Bouton de chat WhatsApp" 1539 1540 msgid "Add a free one-tap WhatsApp chat button to your store. It works with no GoUltra account: it opens a WhatsApp chat to your number, using a standard click-to-chat link." 1541 msgstr "Ajoutez à votre boutique un bouton de chat WhatsApp gratuit en un clic. Il fonctionne sans compte GoUltra : il ouvre un chat WhatsApp vers votre numéro via un lien de chat standard." 1542 1543 msgid "Show the floating chat button" 1544 msgstr "Afficher le bouton de chat flottant" 1545 1546 msgid "A round WhatsApp button in the corner of your storefront." 1547 msgstr "Un bouton WhatsApp rond dans le coin de votre boutique." 1548 1549 msgid "Include the country code. This is the number that will receive the chats." 1550 msgstr "Incluez l'indicatif du pays. C'est le numéro qui recevra les discussions." 1551 1552 msgid "Pre-filled message" 1553 msgstr "Message pré-rempli" 1554 1555 msgid "Hi! I have a question about your store." 1556 msgstr "Bonjour ! J'ai une question sur votre boutique." 1557 1558 msgid "The message the customer starts with. Leave empty for none." 1559 msgstr "Le message par lequel le client commence. Laissez vide pour aucun." 1560 1561 msgid "Button label" 1562 msgstr "Libellé du bouton" 1563 1564 msgid "Position" 1565 msgstr "Position" 1566 1567 msgid "Bottom right" 1568 msgstr "En bas à droite" 1569 1570 msgid "Bottom left" 1571 msgstr "En bas à gauche" 1572 1573 msgid "Show on" 1574 msgstr "Afficher sur" 1575 1576 msgid "All pages" 1577 msgstr "Toutes les pages" 1578 1579 msgid "Shop and product pages only" 1580 msgstr "Pages boutique et produit uniquement" 1581 1582 msgid "Add a button on product pages" 1583 msgstr "Ajouter un bouton sur les pages produit" 1584 1585 msgid "Shows an \"Ask about this product\" WhatsApp button under Add to cart, with the product name filled in." 1586 msgstr "Affiche un bouton WhatsApp \"Poser une question sur ce produit\" sous Ajouter au panier, avec le nom du produit." 1587 1588 msgid "Tip: you can also place a button anywhere with the shortcode" 1589 msgstr "Astuce : vous pouvez aussi placer un bouton n'importe où avec le code court" -
goultra-for-woocommerce/trunk/languages/goultra-for-woocommerce-he_IL.po
r3603449 r3617733 26 26 27 27 msgid "Analytics" 28 msgstr " אנליטיקס"28 msgstr "דוחות" 29 29 30 30 msgid "API key is required." … … 913 913 msgid "Send a test message (optional)" 914 914 msgstr "שליחת הודעת בדיקה (אופציונלי)" 915 916 msgid "Campaigns" 917 msgstr "קמפיינים" 918 919 msgid "New campaign" 920 msgstr "קמפיין חדש" 921 922 msgid "Compose a WhatsApp campaign for your Marketing List subscribers. Unsubscribed contacts are always filtered out automatically." 923 msgstr "חברו קמפיין וואטסאפ לנמעני רשימת התפוצה שלכם. אנשי קשר שהוסרו מסוננים תמיד אוטומטית." 924 925 msgid "Connect your store to GoUltra to send campaigns." 926 msgstr "חברו את החנות ל-GoUltra כדי לשלוח קמפיינים." 927 928 msgid "Template" 929 msgstr "תבנית" 930 931 msgid "Refresh" 932 msgstr "רענון" 933 934 msgid "No approved marketing templates yet. Create one below." 935 msgstr "אין עדיין תבניות שיווקיות מאושרות. צרו אחת למטה." 936 937 msgid "Variables" 938 msgstr "משתנים" 939 940 msgid "Header image" 941 msgstr "תמונת כותרת" 942 943 msgid "Media Library" 944 msgstr "ספריית המדיה" 945 946 msgid "Pick a product" 947 msgstr "בחירת מוצר" 948 949 msgid "Search products" 950 msgstr "חיפוש מוצרים" 951 952 msgid "Send a test to your phone first" 953 msgstr "שלחו קודם בדיקה לטלפון שלכם" 954 955 msgid "Send test" 956 msgstr "שליחת בדיקה" 957 958 msgid "Test sent. Check your WhatsApp." 959 msgstr "הבדיקה נשלחה. בדקו את הוואטסאפ שלכם." 960 961 msgid "Audience" 962 msgstr "קהל יעד" 963 964 msgid "When to send" 965 msgstr "מתי לשלוח" 966 967 msgid "Send now" 968 msgstr "לשלוח עכשיו" 969 970 msgid "Schedule for later" 971 msgstr "לתזמן למועד אחר" 972 973 msgid "Launch campaign" 974 msgstr "שיגור הקמפיין" 975 976 msgid "online" 977 msgstr "מחובר/ת" 978 979 msgid "Choose a template to see the preview." 980 msgstr "בחרו תבנית כדי לראות את התצוגה המקדימה." 981 982 msgid "Campaign history" 983 msgstr "היסטוריית קמפיינים" 984 985 msgid "Sent" 986 msgstr "נשלחו" 987 988 msgid "Failed" 989 msgstr "נכשלו" 990 991 msgid "Create a marketing template" 992 msgstr "יצירת תבנית שיווקית" 993 994 msgid "Pick a language and style, adjust the text, and submit for Meta approval. A localized opt-out footer is added automatically, as Meta requires for marketing." 995 msgstr "בחרו שפה וסגנון, התאימו את הטקסט ושלחו לאישור Meta. שורת הסרה בשפת התבנית מתווספת אוטומטית, כפי ש-Meta דורשת לשיווק." 996 997 msgid "Language" 998 msgstr "שפה" 999 1000 msgid "Style" 1001 msgstr "סגנון" 1002 1003 msgid "Message text" 1004 msgstr "טקסט ההודעה" 1005 1006 msgid "Button label (optional)" 1007 msgstr "כיתוב כפתור (אופציונלי)" 1008 1009 msgid "Button link (optional)" 1010 msgstr "קישור כפתור (אופציונלי)" 1011 1012 msgid "Submit for approval" 1013 msgstr "שליחה לאישור" 1014 1015 msgid "Something went wrong. Please try again." 1016 msgstr "משהו השתבש. נסו שוב." 1017 1018 msgid "Choose a template" 1019 msgstr "בחרו תבנית" 1020 1021 msgid "Choose a template first." 1022 msgstr "בחרו תבנית קודם." 1023 1024 msgid "Awaiting Meta approval:" 1025 msgstr "ממתינות לאישור Meta:" 1026 1027 msgid "Customer first name" 1028 msgstr "שם פרטי של הלקוח" 1029 1030 msgid "Customer full name" 1031 msgstr "שם מלא של הלקוח" 1032 1033 msgid "Fixed value" 1034 msgstr "ערך קבוע" 1035 1036 msgid "No products with a usable image were found." 1037 msgstr "לא נמצאו מוצרים עם תמונה מתאימה." 1038 1039 msgid "Sending..." 1040 msgstr "שולח..." 1041 1042 msgid "Please fill in all template variables." 1043 msgstr "מלאו את כל משתני התבנית." 1044 1045 msgid "This template needs a header image. Choose one from the media library, a product, or paste an https image link." 1046 msgstr "התבנית הזו צריכה תמונת כותרת. בחרו מספריית המדיה, ממוצר, או הדביקו קישור https לתמונה." 1047 1048 msgid "%d subscribers will receive this campaign." 1049 msgstr "%d נמענים יקבלו את הקמפיין הזה." 1050 1051 msgid "Send this campaign to %d subscribers now?" 1052 msgstr "לשלוח את הקמפיין ל-%d נמענים עכשיו?" 1053 1054 msgid "Schedule this campaign to %d subscribers?" 1055 msgstr "לתזמן את הקמפיין ל-%d נמענים?" 1056 1057 msgid "Campaign launched. Messages are on their way." 1058 msgstr "הקמפיין שוגר. ההודעות בדרך." 1059 1060 msgid "Campaign scheduled." 1061 msgstr "הקמפיין תוזמן." 1062 1063 msgid "No campaigns yet." 1064 msgstr "אין עדיין קמפיינים." 1065 1066 msgid "Pause" 1067 msgstr "השהיה" 1068 1069 msgid "Cancel" 1070 msgstr "ביטול" 1071 1072 msgid "Pause this campaign?" 1073 msgstr "להשהות את הקמפיין?" 1074 1075 msgid "Cancel this scheduled campaign?" 1076 msgstr "לבטל את הקמפיין המתוזמן?" 1077 1078 msgid "Processing" 1079 msgstr "בתהליך" 1080 1081 msgid "Scheduled" 1082 msgstr "מתוזמן" 1083 1084 msgid "Completed" 1085 msgstr "הושלם" 1086 1087 msgid "Cancelled" 1088 msgstr "בוטל" 1089 1090 msgid "Paused" 1091 msgstr "מושהה" 1092 1093 msgid "Opt-out footer (added automatically):" 1094 msgstr "שורת הסרה (מתווספת אוטומטית):" 1095 1096 msgid "The message text must be between 15 and 1024 characters." 1097 msgstr "טקסט ההודעה חייב להיות בין 15 ל-1024 תווים." 1098 1099 msgid "Template submitted. Meta approval usually takes minutes to a few hours." 1100 msgstr "התבנית נשלחה. אישור Meta לוקח בדרך כלל בין דקות לכמה שעות." 1101 1102 msgid "A button needs both a label and a link." 1103 msgstr "כפתור צריך גם כיתוב וגם קישור." 1104 1105 msgid "Could not submit the template. Please try again." 1106 msgstr "לא ניתן לשלוח את התבנית. נסו שוב." 1107 1108 msgid "Could not load templates. Please try again." 1109 msgstr "לא ניתן לטעון את התבניות. נסו שוב." 1110 1111 msgid "Test send failed. Please try again." 1112 msgstr "שליחת הבדיקה נכשלה. נסו שוב." 1113 1114 msgid "Could not launch the campaign. Please try again." 1115 msgstr "לא ניתן לשגר את הקמפיין. נסו שוב." 1116 1117 msgid "The action failed. Please try again." 1118 msgstr "הפעולה נכשלה. נסו שוב." 1119 1120 msgid "Invalid request." 1121 msgstr "בקשה לא תקינה." 1122 1123 msgid "Choose a valid date and time for the scheduled send." 1124 msgstr "בחרו תאריך ושעה תקינים לשליחה המתוזמנת." 1125 1126 msgid "A campaign was just launched. Please wait two minutes before launching another." 1127 msgstr "קמפיין שוגר ממש עכשיו. המתינו שתי דקות לפני שיגור נוסף." 1128 1129 msgid "Your audience is larger than 5,000 contacts. Please run this campaign from the GoUltra dashboard." 1130 msgstr "קהל היעד גדול מ-5,000 אנשי קשר. הריצו את הקמפיין הזה מלוח הבקרה של GoUltra." 1131 1132 msgid "Your GoUltra account has more than one WhatsApp number connected, so the sending number must be chosen in the GoUltra dashboard for this store." 1133 msgstr "לחשבון ה-GoUltra שלכם מחוברים כמה מספרי וואטסאפ, ולכן יש לבחור את מספר השליחה של החנות בלוח הבקרה של GoUltra." 1134 1135 msgid "Your GoUltra plan message allowance for this cycle has been reached. Upgrade your plan to send more." 1136 msgstr "מכסת ההודעות של מסלול ה-GoUltra שלכם למחזור הזה נוצלה. שדרגו את המסלול כדי לשלוח עוד." 1137 1138 msgid "GoUltra for WooCommerce - WhatsApp Marketing, Notifications & Abandoned Cart" 1139 msgstr "GoUltra ל-WooCommerce - שיווק, התראות ועגלה נטושה בוואטסאפ" 1140 1141 msgid "WooCommerce WhatsApp notifications: automated order, shipping and abandoned-cart messages, plus opt-in marketing campaigns, powered by GoUltra." 1142 msgstr "התראות WhatsApp ל-WooCommerce: הודעות אוטומטיות לאישור הזמנה, משלוח ועגלה נטושה, וקמפיינים שיווקיים בהסכמה, באמצעות GoUltra." 1143 1144 msgid "Marketing Offer" 1145 msgstr "מבצע שיווקי" 1146 1147 msgid "Promotions and offers for opted-in subscribers" 1148 msgstr "מבצעים והצעות לנמענים שנרשמו" 1149 1150 msgid "Recipients" 1151 msgstr "נמענים" 1152 1153 msgid "All subscribers" 1154 msgstr "כל הנמענים" 1155 1156 msgid "Choose specific contacts" 1157 msgstr "בחירת אנשי קשר מסוימים" 1158 1159 msgid "Search name or number" 1160 msgstr "חיפוש שם או מספר" 1161 1162 msgid "Select all" 1163 msgstr "בחירת הכל" 1164 1165 msgid "Clear" 1166 msgstr "ניקוי" 1167 1168 msgid "Also send to my own number (%s) for a test" 1169 msgstr "לשלוח גם למספר שלי (%s) לבדיקה" 1170 1171 msgid "Also send to my own number for a test (set your admin number in Settings)" 1172 msgstr "לשלוח גם למספר שלי לבדיקה (הגדירו את מספר המנהל בהגדרות)" 1173 1174 msgid "Need a marketing template?" 1175 msgstr "צריכים תבנית שיווקית?" 1176 1177 msgid "Create one in the Templates tab (Marketing Offer): pick a language and ready-made style, and the opt-out footer is added automatically. Approved templates appear in the picker above." 1178 msgstr "צרו אותה בלשונית תבניות (מבצע שיווקי): בחרו שפה וסגנון מוכן, ושורת ההסרה מתווספת אוטומטית. תבניות מאושרות מופיעות בבורר למעלה." 1179 1180 msgid "Open Templates" 1181 msgstr "פתיחת תבניות" 1182 1183 msgid "Delivery receipts and product sync" 1184 msgstr "אישורי מסירה וסנכרון מוצרים" 1185 1186 msgid "These work automatically once your store is connected. There is nothing to set up." 1187 msgstr "אלה עובדים אוטומטית לאחר חיבור החנות. אין מה להגדיר." 1188 1189 msgid "Connected and active." 1190 msgstr "מחובר ופעיל." 1191 1192 msgid "Message delivery receipts appear as order notes, and your product catalog is available to GoUltra automatically. No secret or URL to copy anywhere." 1193 msgstr "אישורי מסירת הודעות מופיעים כהערות בהזמנה, וקטלוג המוצרים שלכם זמין ל-GoUltra אוטומטית. אין סוד או כתובת להעתיק." 1194 1195 msgid "Technical details (for support)" 1196 msgstr "פרטים טכניים (לתמיכה)" 1197 1198 msgid "Webhook secret" 1199 msgstr "סוד Webhook" 1200 1201 msgid "The plugin shares the secret with GoUltra automatically on connect, so you never paste it anywhere." 1202 msgstr "התוסף משתף את הסוד עם GoUltra אוטומטית בעת החיבור, כך שאתם לא צריכים להדביק אותו בשום מקום." 1203 1204 msgid "All subscribers (%d)" 1205 msgstr "כל הנמענים (%d)" 1206 1207 msgid "%d selected" 1208 msgstr "%d נבחרו" 1209 1210 msgid "No subscribers yet." 1211 msgstr "אין עדיין נמענים." 1212 1213 msgid "No recipients selected. Choose subscribers, or add your own number to send yourself a test." 1214 msgstr "לא נבחרו נמענים. בחרו נמענים, או הוסיפו את המספר שלכם כדי לשלוח לעצמכם בדיקה." 1215 1216 msgid "%d recipients will receive this campaign." 1217 msgstr "%d נמענים יקבלו את הקמפיין הזה." 1218 1219 msgid "Send this campaign to %d recipients now?" 1220 msgstr "לשלוח את הקמפיין ל-%d נמענים עכשיו?" 1221 1222 msgid "Schedule this campaign to %d recipients?" 1223 msgstr "לתזמן את הקמפיין ל-%d נמענים?" 1224 1225 msgid "Add my number" 1226 msgstr "הוספת המספר שלי" 1227 1228 msgid "Add to the list" 1229 msgstr "הוספה לרשימה" 1230 1231 msgid "Adding..." 1232 msgstr "מוסיף..." 1233 1234 msgid "Added to the list." 1235 msgstr "נוסף לרשימה." 1236 1237 msgid "Could not add the contact. Please try again." 1238 msgstr "לא ניתן להוסיף את איש הקשר. נסו שוב." 1239 1240 msgid "Adds the number to the marketing list as a subscribed contact. Useful for sending yourself a test campaign: on the Campaigns screen choose \"Choose specific contacts\" and select only your number." 1241 msgstr "מוסיף את המספר לרשימת התפוצה כנמען רשום. שימושי לשליחת קמפיין בדיקה לעצמכם: במסך הקמפיינים בחרו \"בחירת אנשי קשר מסוימים\" וסמנו רק את המספר שלכם." 1242 1243 msgid "Tip: to test on yourself, add your own number on the Marketing List screen, then select only it here." 1244 msgstr "טיפ: לבדיקה על עצמכם, הוסיפו את המספר שלכם במסך רשימת התפוצה ואז סמנו כאן רק אותו." 1245 1246 msgid "No recipients selected. Choose contacts from the list." 1247 msgstr "לא נבחרו נמענים. בחרו אנשי קשר מהרשימה." 1248 1249 msgid "No name" 1250 msgstr "ללא שם" 1251 1252 msgid "Today" 1253 msgstr "היום" 1254 1255 msgid "Your GoUltra account is currently on hold, so messages cannot be sent. Please contact GoUltra support to resume." 1256 msgstr "חשבון ה-GoUltra שלכם מושהה כרגע, ולכן לא ניתן לשלוח הודעות. פנו לתמיכת GoUltra כדי לחדש." 1257 1258 msgid "Edit name" 1259 msgstr "עריכת שם" 1260 1261 msgid "Schedule campaign" 1262 msgstr "תזמון הקמפיין" 1263 1264 msgid "Will be sent on %1$s at %2$s (your store time)." 1265 msgstr "יישלח בתאריך %1$s בשעה %2$s (לפי שעון החנות)." 1266 1267 msgid "Could not update the name. Please try again." 1268 msgstr "לא ניתן לעדכן את השם. נסו שוב." 1269 1270 msgid "GoUltra returned HTTP %d." 1271 msgstr "GoUltra החזירה HTTP %d." 1272 1273 msgid "Template name" 1274 msgstr "שם התבנית" 1275 1276 msgid "Lowercase letters, numbers and underscores only." 1277 msgstr "אותיות קטנות באנגלית, ספרות וקו תחתון בלבד." 1278 1279 msgid "Marketing (offers, promotions)" 1280 msgstr "שיווקי (מבצעים, הצעות)" 1281 1282 msgid "Utility (order, account updates)" 1283 msgstr "שירותי (הזמנה, עדכוני חשבון)" 1284 1285 msgid "No header" 1286 msgstr "ללא כותרת" 1287 1288 msgid "Text" 1289 msgstr "טקסט" 1290 1291 msgid "Video" 1292 msgstr "וידאו" 1293 1294 msgid "Document" 1295 msgstr "מסמך" 1296 1297 msgid "Header text" 1298 msgstr "טקסט הכותרת" 1299 1300 msgid "You will choose the actual image/video/file when you send a campaign." 1301 msgstr "תבחרו את התמונה/וידאו/קובץ בפועל בעת שליחת קמפיין." 1302 1303 msgid "Note:" 1304 msgstr "לתשומת לב:" 1305 1306 msgid "Utility templates are for transactional messages only (order, shipping, account). Promotional content will be rejected by Meta." 1307 msgstr "תבניות שירותיות מיועדות להודעות טרנזקציוניות בלבד (הזמנה, משלוח, חשבון). תוכן פרסומי יידחה על ידי Meta." 1308 1309 msgid "Insert a variable" 1310 msgstr "הוספת משתנה" 1311 1312 msgid "(filled in automatically at send time)" 1313 msgstr "(מתמלא אוטומטית בעת השליחה)" 1314 1315 msgid "Buttons" 1316 msgstr "כפתורים" 1317 1318 msgid "(up to 10, optional)" 1319 msgstr "(עד 10, אופציונלי)" 1320 1321 msgid "Add button" 1322 msgstr "הוספת כפתור" 1323 1324 msgid "Opt-out method (required for marketing)" 1325 msgstr "שיטת הסרה (חובה לשיווקי)" 1326 1327 msgid "Footer text (recommended)" 1328 msgstr "טקסט תחתון (מומלץ)" 1329 1330 msgid "Unsubscribe button" 1331 msgstr "כפתור הסרה" 1332 1333 msgid "Footer" 1334 msgstr "טקסט תחתון" 1335 1336 msgid "(small gray line, optional)" 1337 msgstr "(שורה אפורה קטנה, אופציונלי)" 1338 1339 msgid "Quick reply" 1340 msgstr "תגובה מהירה" 1341 1342 msgid "URL link" 1343 msgstr "קישור URL" 1344 1345 msgid "Phone number" 1346 msgstr "מספר טלפון" 1347 1348 msgid "Button text" 1349 msgstr "טקסט הכפתור" 1350 1351 msgid "A URL or phone button needs a value." 1352 msgstr "כפתור URL או טלפון דורש ערך." 1353 1354 msgid "Remove" 1355 msgstr "הסרה" 1356 1357 msgid "Two variables cannot be next to each other. Add some text between them (Meta rejects it otherwise)." 1358 msgstr "שני משתנים לא יכולים להיות זה לצד זה. הוסיפו טקסט ביניהם (אחרת Meta תדחה)." 1359 1360 msgid "The message text must be between 5 and 1024 characters." 1361 msgstr "טקסט ההודעה חייב להיות בין 5 ל-1024 תווים." 1362 1363 msgid "Meta rejected the template. %1$s (fallback: %2$s)" 1364 msgstr "Meta דחתה את התבנית. %1$s (גיבוי: %2$s)" 1365 1366 msgid "Unknown error." 1367 msgstr "שגיאה לא ידועה." 1368 1369 msgid "The template could not be submitted. %s" 1370 msgstr "לא ניתן היה להגיש את התבנית. %s" 1371 1372 msgid "This number is not on your marketing list, so there is no name to update." 1373 msgstr "המספר הזה אינו ברשימת השיווק שלכם, ולכן אין שם לעדכן." 1374 1375 msgid "Paste your API key (it starts with gk_) from the GoUltra Dashboard → Settings → API." 1376 msgstr "הדביקו את מפתח ה-API (מתחיל ב-gk_) מלוח GoUltra → הגדרות → API." 1377 1378 msgid "Campaign name (optional)" 1379 msgstr "שם הקמפיין (אופציונלי)" 1380 1381 msgid "e.g. Summer Sale - July" 1382 msgstr "לדוגמה: מבצע קיץ - יולי" 1383 1384 msgid "A name to recognize this campaign in your history." 1385 msgstr "שם שיעזור לזהות את הקמפיין בהיסטוריה." 1386 1387 msgid "Upload / Media Library" 1388 msgstr "העלאה / ספריית המדיה" 1389 1390 msgid "Image selected" 1391 msgstr "תמונה נבחרה" 1392 1393 msgid "Or paste an image URL" 1394 msgstr "או הדביקו כתובת URL של תמונה" 1395 1396 msgid "JPEG or PNG images only. Choose from your Media Library, pick a store product, or paste a URL." 1397 msgstr "תמונות JPEG או PNG בלבד. בחרו מספריית המדיה, בחרו מוצר מהחנות, או הדביקו כתובת URL." 1398 1399 msgid "Enter a valid WhatsApp number, including the country code." 1400 msgstr "הזינו מספר WhatsApp תקין, כולל קידומת מדינה." 1401 1402 msgid "Too many code requests. Please wait a while before trying again." 1403 msgstr "יותר מדי בקשות קוד. המתינו מעט ונסו שוב." 1404 1405 msgid "Your WhatsApp verification template is still being approved by Meta (usually a few minutes, sometimes longer). Please try again shortly - nothing else is needed from you." 1406 msgstr "תבנית האימות שלכם ב-WhatsApp עדיין בתהליך אישור אצל Meta (בדרך כלל כמה דקות, לעיתים יותר). נסו שוב עוד מעט - לא נדרש מכם דבר נוסף." 1407 1408 msgid "Could not send the code. Please check the number and try again." 1409 msgstr "לא ניתן היה לשלוח את הקוד. בדקו את המספר ונסו שוב." 1410 1411 msgid "We sent a 6-digit code to %s on WhatsApp. Enter it below." 1412 msgstr "שלחנו קוד בן 6 ספרות אל %s ב-WhatsApp. הזינו אותו למטה." 1413 1414 msgid "The code expired. Please request a new one." 1415 msgstr "הקוד פג. בקשו קוד חדש." 1416 1417 msgid "Too many wrong attempts. Please request a new code." 1418 msgstr "יותר מדי ניסיונות שגויים. בקשו קוד חדש." 1419 1420 msgid "Incorrect code. Attempts left: %d." 1421 msgstr "קוד שגוי. ניסיונות שנותרו: %d." 1422 1423 msgid "Your number is verified. It is now your store manager number and can be selected as a campaign recipient for your own tests." 1424 msgstr "המספר שלכם אומת. הוא כעת מספר מנהל החנות שלכם, וניתן לבחור אותו כנמען בקמפיין לבדיקות שלכם." 1425 1426 msgid "Store manager" 1427 msgstr "מנהל החנות" 1428 1429 msgid "Verify your number (recommended)" 1430 msgstr "אמתו את המספר שלכם (מומלץ)" 1431 1432 msgid "Confirm your WhatsApp number with a code. A verified number becomes your store manager number, which you can select as a campaign recipient to test your own campaigns." 1433 msgstr "אשרו את מספר ה-WhatsApp שלכם באמצעות קוד. מספר מאומת הופך למספר מנהל החנות, שאותו תוכלו לבחור כנמען בקמפיין כדי לבדוק את הקמפיינים שלכם." 1434 1435 msgid "Verified" 1436 msgstr "מאומת" 1437 1438 msgid "Verify a different number" 1439 msgstr "אימות מספר אחר" 1440 1441 msgid "Send code" 1442 msgstr "שליחת קוד" 1443 1444 msgid "Verify" 1445 msgstr "אימות" 1446 1447 msgid "Resend code" 1448 msgstr "שליחה חוזרת של הקוד" 1449 1450 msgid "Verifying..." 1451 msgstr "מאמת..." 1452 1453 msgid "Enter your WhatsApp number first." 1454 msgstr "הזינו קודם את מספר ה-WhatsApp שלכם." 1455 1456 msgid "Enter the code you received." 1457 msgstr "הזינו את הקוד שקיבלתם." 1458 1459 msgid "Could not send the code." 1460 msgstr "לא ניתן היה לשלוח את הקוד." 1461 1462 msgid "Incorrect code." 1463 msgstr "קוד שגוי." 1464 1465 msgid "Network error. Please try again." 1466 msgstr "שגיאת רשת. נסו שוב." 1467 1468 msgid "Building a template for a product?" 1469 msgstr "בונים תבנית עבור מוצר?" 1470 1471 msgid "Pick a store product and we set up the template for it: an image header plus a \"View product\" button that links to it. Then choose a language and submit for approval." 1472 msgstr "בחרו מוצר מהחנות ונכין עבורו את התבנית: כותרת תמונה וכפתור \"צפייה במוצר\" שמקשר אליו. אחר כך בחרו שפה ושלחו לאישור." 1473 1474 msgid "View product" 1475 msgstr "צפייה במוצר" 1476 1477 msgid "No products found." 1478 msgstr "לא נמצאו מוצרים." 1479 1480 msgid "You already have an approved or pending template of this type for this language. Create another one?" 1481 msgstr "כבר יש לכם תבנית מאושרת או ממתינה מסוג זה בשפה הזאת. ליצור עוד אחת?" 1482 1483 msgid "Unconfirmed - verify before resending" 1484 msgstr "לא אושר - לבדוק לפני שליחה חוזרת" 1485 1486 msgid "The connection dropped before we got a reply, so the campaign may or may not have gone out. Check Campaign History and your GoUltra dashboard before launching it again to avoid sending it twice." 1487 msgstr "החיבור נותק לפני שקיבלנו תשובה, כך שייתכן שהקמפיין נשלח וייתכן שלא. בדקו בהיסטוריית הקמפיינים ובלוח הבקרה של GoUltra לפני שליחה חוזרת, כדי לא לשלוח פעמיים." 1488 1489 msgid "GoUltra for WooCommerce: connection problem." 1490 msgstr "GoUltra ל-WooCommerce: בעיית חיבור." 1491 1492 msgid "Recent WhatsApp messages were rejected because the API key is no longer valid (it may have been regenerated or the account changed). Messages are not being delivered." 1493 msgstr "הודעות WhatsApp אחרונות נדחו כי מפתח ה-API כבר לא תקף (ייתכן שנוצר מחדש או שהחשבון השתנה). הודעות אינן נשלחות." 1494 1495 msgid "Reconnect your store" 1496 msgstr "חברו מחדש את החנות" 1497 1498 msgid "Image" 1499 msgstr "תמונה" 1500 1501 msgid "View" 1502 msgstr "צפייה" 1503 1504 msgid "rejected" 1505 msgstr "נדחתה" 1506 1507 msgid "No templates match your search." 1508 msgstr "אין תבניות שתואמות את החיפוש." 1509 1510 msgid "Search your templates by name, text or language..." 1511 msgstr "חיפוש בתבניות שלך לפי שם, טקסט או שפה..." 1512 1513 msgid "Close" 1514 msgstr "סגירה" 1515 1516 msgid "GoUltra did not accept this store's key for this action. Your connection is fine (order messages work), but campaign sending for this number may not be enabled yet on your GoUltra account. Please contact GoUltra support to enable it." 1517 msgstr "GoUltra לא קיבלה את המפתח של החנות עבור פעולה זו. החיבור שלכם תקין (הודעות הזמנה נשלחות), אבל ייתכן ששליחת קמפיינים למספר הזה עדיין לא הופעלה בחשבון GoUltra שלכם. אנא פנו לתמיכת GoUltra כדי להפעיל אותה." 1518 1519 msgid "JPEG or PNG images only." 1520 msgstr "תמונות JPEG או PNG בלבד." 1521 1522 msgid "Marketing sending is turned off for this WhatsApp number. Enable marketing for this number in your GoUltra dashboard to send campaigns." 1523 msgstr "שליחת הודעות שיווקיות כבויה עבור מספר ה-WhatsApp הזה. הפעילו שיווק למספר הזה בלוח הבקרה של GoUltra כדי לשלוח קמפיינים." 1524 1525 msgid "Chat with us" 1526 msgstr "צ'אט איתנו" 1527 1528 msgid "Chat on WhatsApp" 1529 msgstr "צ'אט בוואטסאפ" 1530 1531 msgid "about %s" 1532 msgstr "לגבי %s" 1533 1534 msgid "Ask about this product on WhatsApp" 1535 msgstr "שאלו על המוצר הזה בוואטסאפ" 1536 1537 msgid "WhatsApp Chat Button" 1538 msgstr "כפתור צ'אט וואטסאפ" 1539 1540 msgid "Add a free one-tap WhatsApp chat button to your store. It works with no GoUltra account: it opens a WhatsApp chat to your number, using a standard click-to-chat link." 1541 msgstr "הוסיפו לחנות כפתור צ'אט וואטסאפ חינמי בלחיצה אחת. הוא עובד ללא חשבון GoUltra: הוא פותח צ'אט וואטסאפ למספר שלכם, באמצעות קישור צ'אט רגיל." 1542 1543 msgid "Show the floating chat button" 1544 msgstr "הצג את כפתור הצ'אט הצף" 1545 1546 msgid "A round WhatsApp button in the corner of your storefront." 1547 msgstr "כפתור וואטסאפ עגול בפינת החנות שלכם." 1548 1549 msgid "Include the country code. This is the number that will receive the chats." 1550 msgstr "כללו את קידומת המדינה. זהו המספר שיקבל את הצ'אטים." 1551 1552 msgid "Pre-filled message" 1553 msgstr "הודעה מוכנה מראש" 1554 1555 msgid "Hi! I have a question about your store." 1556 msgstr "שלום! יש לי שאלה על החנות שלכם." 1557 1558 msgid "The message the customer starts with. Leave empty for none." 1559 msgstr "ההודעה שבה הלקוח מתחיל. השאירו ריק כדי לא להגדיר." 1560 1561 msgid "Button label" 1562 msgstr "תווית הכפתור" 1563 1564 msgid "Position" 1565 msgstr "מיקום" 1566 1567 msgid "Bottom right" 1568 msgstr "למטה מימין" 1569 1570 msgid "Bottom left" 1571 msgstr "למטה משמאל" 1572 1573 msgid "Show on" 1574 msgstr "הצג ב" 1575 1576 msgid "All pages" 1577 msgstr "כל העמודים" 1578 1579 msgid "Shop and product pages only" 1580 msgstr "עמודי חנות ומוצר בלבד" 1581 1582 msgid "Add a button on product pages" 1583 msgstr "הוסיפו כפתור בעמודי מוצר" 1584 1585 msgid "Shows an \"Ask about this product\" WhatsApp button under Add to cart, with the product name filled in." 1586 msgstr "מציג כפתור וואטסאפ \"שאלו על המוצר הזה\" מתחת ללחצן הוספה לעגלה, עם שם המוצר." 1587 1588 msgid "Tip: you can also place a button anywhere with the shortcode" 1589 msgstr "טיפ: תוכלו למקם כפתור בכל מקום בעזרת השורטקוד" -
goultra-for-woocommerce/trunk/languages/goultra-for-woocommerce.pot
r3603449 r3617733 1230 1230 msgid "Send a test message (optional)" 1231 1231 msgstr "" 1232 1233 msgid "Campaigns" 1234 msgstr "" 1235 1236 msgid "New campaign" 1237 msgstr "" 1238 1239 msgid "Compose a WhatsApp campaign for your Marketing List subscribers. Unsubscribed contacts are always filtered out automatically." 1240 msgstr "" 1241 1242 msgid "Connect your store to GoUltra to send campaigns." 1243 msgstr "" 1244 1245 msgid "Template" 1246 msgstr "" 1247 1248 msgid "Refresh" 1249 msgstr "" 1250 1251 msgid "No approved marketing templates yet. Create one below." 1252 msgstr "" 1253 1254 msgid "Variables" 1255 msgstr "" 1256 1257 msgid "Header image" 1258 msgstr "" 1259 1260 msgid "Media Library" 1261 msgstr "" 1262 1263 msgid "Pick a product" 1264 msgstr "" 1265 1266 msgid "Search products" 1267 msgstr "" 1268 1269 msgid "Send a test to your phone first" 1270 msgstr "" 1271 1272 msgid "Send test" 1273 msgstr "" 1274 1275 msgid "Test sent. Check your WhatsApp." 1276 msgstr "" 1277 1278 msgid "Audience" 1279 msgstr "" 1280 1281 msgid "When to send" 1282 msgstr "" 1283 1284 msgid "Send now" 1285 msgstr "" 1286 1287 msgid "Schedule for later" 1288 msgstr "" 1289 1290 msgid "Launch campaign" 1291 msgstr "" 1292 1293 msgid "online" 1294 msgstr "" 1295 1296 msgid "Choose a template to see the preview." 1297 msgstr "" 1298 1299 msgid "Campaign history" 1300 msgstr "" 1301 1302 msgid "Sent" 1303 msgstr "" 1304 1305 msgid "Failed" 1306 msgstr "" 1307 1308 msgid "Create a marketing template" 1309 msgstr "" 1310 1311 msgid "Pick a language and style, adjust the text, and submit for Meta approval. A localized opt-out footer is added automatically, as Meta requires for marketing." 1312 msgstr "" 1313 1314 msgid "Language" 1315 msgstr "" 1316 1317 msgid "Style" 1318 msgstr "" 1319 1320 msgid "Message text" 1321 msgstr "" 1322 1323 msgid "Button label (optional)" 1324 msgstr "" 1325 1326 msgid "Button link (optional)" 1327 msgstr "" 1328 1329 msgid "Submit for approval" 1330 msgstr "" 1331 1332 msgid "Something went wrong. Please try again." 1333 msgstr "" 1334 1335 msgid "Choose a template" 1336 msgstr "" 1337 1338 msgid "Choose a template first." 1339 msgstr "" 1340 1341 msgid "Awaiting Meta approval:" 1342 msgstr "" 1343 1344 msgid "Customer first name" 1345 msgstr "" 1346 1347 msgid "Customer full name" 1348 msgstr "" 1349 1350 msgid "Fixed value" 1351 msgstr "" 1352 1353 msgid "No products with a usable image were found." 1354 msgstr "" 1355 1356 msgid "Sending..." 1357 msgstr "" 1358 1359 msgid "Please fill in all template variables." 1360 msgstr "" 1361 1362 msgid "This template needs a header image. Choose one from the media library, a product, or paste an https image link." 1363 msgstr "" 1364 1365 msgid "%d subscribers will receive this campaign." 1366 msgstr "" 1367 1368 msgid "Send this campaign to %d subscribers now?" 1369 msgstr "" 1370 1371 msgid "Schedule this campaign to %d subscribers?" 1372 msgstr "" 1373 1374 msgid "Campaign launched. Messages are on their way." 1375 msgstr "" 1376 1377 msgid "Campaign scheduled." 1378 msgstr "" 1379 1380 msgid "No campaigns yet." 1381 msgstr "" 1382 1383 msgid "Pause" 1384 msgstr "" 1385 1386 msgid "Cancel" 1387 msgstr "" 1388 1389 msgid "Pause this campaign?" 1390 msgstr "" 1391 1392 msgid "Cancel this scheduled campaign?" 1393 msgstr "" 1394 1395 msgid "Processing" 1396 msgstr "" 1397 1398 msgid "Scheduled" 1399 msgstr "" 1400 1401 msgid "Completed" 1402 msgstr "" 1403 1404 msgid "Cancelled" 1405 msgstr "" 1406 1407 msgid "Paused" 1408 msgstr "" 1409 1410 msgid "Opt-out footer (added automatically):" 1411 msgstr "" 1412 1413 msgid "The message text must be between 15 and 1024 characters." 1414 msgstr "" 1415 1416 msgid "Template submitted. Meta approval usually takes minutes to a few hours." 1417 msgstr "" 1418 1419 msgid "A button needs both a label and a link." 1420 msgstr "" 1421 1422 msgid "Could not submit the template. Please try again." 1423 msgstr "" 1424 1425 msgid "Could not load templates. Please try again." 1426 msgstr "" 1427 1428 msgid "Test send failed. Please try again." 1429 msgstr "" 1430 1431 msgid "Could not launch the campaign. Please try again." 1432 msgstr "" 1433 1434 msgid "The action failed. Please try again." 1435 msgstr "" 1436 1437 msgid "Invalid request." 1438 msgstr "" 1439 1440 msgid "Choose a valid date and time for the scheduled send." 1441 msgstr "" 1442 1443 msgid "A campaign was just launched. Please wait two minutes before launching another." 1444 msgstr "" 1445 1446 msgid "Your audience is larger than 5,000 contacts. Please run this campaign from the GoUltra dashboard." 1447 msgstr "" 1448 1449 msgid "Your GoUltra account has more than one WhatsApp number connected, so the sending number must be chosen in the GoUltra dashboard for this store." 1450 msgstr "" 1451 1452 msgid "Your GoUltra plan message allowance for this cycle has been reached. Upgrade your plan to send more." 1453 msgstr "" 1454 1455 msgid "GoUltra for WooCommerce - WhatsApp Order Notifications & Abandoned Cart" 1456 msgstr "" 1457 1458 msgid "Automate WooCommerce WhatsApp order updates, shipping notifications, abandoned cart recovery and opt-in marketing through GoUltra." 1459 msgstr "" 1460 1461 msgid "Marketing Offer" 1462 msgstr "" 1463 1464 msgid "Promotions and offers for opted-in subscribers" 1465 msgstr "" 1466 1467 msgid "Recipients" 1468 msgstr "" 1469 1470 msgid "All subscribers" 1471 msgstr "" 1472 1473 msgid "Choose specific contacts" 1474 msgstr "" 1475 1476 msgid "Search name or number" 1477 msgstr "" 1478 1479 msgid "Select all" 1480 msgstr "" 1481 1482 msgid "Clear" 1483 msgstr "" 1484 1485 msgid "Also send to my own number (%s) for a test" 1486 msgstr "" 1487 1488 msgid "Also send to my own number for a test (set your admin number in Settings)" 1489 msgstr "" 1490 1491 msgid "Need a marketing template?" 1492 msgstr "" 1493 1494 msgid "Create one in the Templates tab (Marketing Offer): pick a language and ready-made style, and the opt-out footer is added automatically. Approved templates appear in the picker above." 1495 msgstr "" 1496 1497 msgid "Open Templates" 1498 msgstr "" 1499 1500 msgid "Delivery receipts and product sync" 1501 msgstr "" 1502 1503 msgid "These work automatically once your store is connected. There is nothing to set up." 1504 msgstr "" 1505 1506 msgid "Connected and active." 1507 msgstr "" 1508 1509 msgid "Message delivery receipts appear as order notes, and your product catalog is available to GoUltra automatically. No secret or URL to copy anywhere." 1510 msgstr "" 1511 1512 msgid "Technical details (for support)" 1513 msgstr "" 1514 1515 msgid "Webhook secret" 1516 msgstr "" 1517 1518 msgid "The plugin shares the secret with GoUltra automatically on connect, so you never paste it anywhere." 1519 msgstr "" 1520 1521 msgid "All subscribers (%d)" 1522 msgstr "" 1523 1524 msgid "%d selected" 1525 msgstr "" 1526 1527 msgid "No subscribers yet." 1528 msgstr "" 1529 1530 msgid "No recipients selected. Choose subscribers, or add your own number to send yourself a test." 1531 msgstr "" 1532 1533 msgid "%d recipients will receive this campaign." 1534 msgstr "" 1535 1536 msgid "Send this campaign to %d recipients now?" 1537 msgstr "" 1538 1539 msgid "Schedule this campaign to %d recipients?" 1540 msgstr "" 1541 1542 msgid "Add my number" 1543 msgstr "" 1544 1545 msgid "Add to the list" 1546 msgstr "" 1547 1548 msgid "Adding..." 1549 msgstr "" 1550 1551 msgid "Added to the list." 1552 msgstr "" 1553 1554 msgid "Could not add the contact. Please try again." 1555 msgstr "" 1556 1557 msgid "Adds the number to the marketing list as a subscribed contact. Useful for sending yourself a test campaign: on the Campaigns screen choose \"Choose specific contacts\" and select only your number." 1558 msgstr "" 1559 1560 msgid "Tip: to test on yourself, add your own number on the Marketing List screen, then select only it here." 1561 msgstr "" 1562 1563 msgid "No recipients selected. Choose contacts from the list." 1564 msgstr "" 1565 1566 msgid "No name" 1567 msgstr "" 1568 1569 msgid "Today" 1570 msgstr "" 1571 1572 msgid "Your GoUltra account is currently on hold, so messages cannot be sent. Please contact GoUltra support to resume." 1573 msgstr "" 1574 1575 msgid "Edit name" 1576 msgstr "" 1577 1578 msgid "Schedule campaign" 1579 msgstr "" 1580 1581 msgid "Will be sent on %1$s at %2$s (your store time)." 1582 msgstr "" 1583 1584 msgid "Could not update the name. Please try again." 1585 msgstr "" 1586 1587 msgid "GoUltra returned HTTP %d." 1588 msgstr "" 1589 1590 msgid "Template name" 1591 msgstr "" 1592 1593 msgid "Lowercase letters, numbers and underscores only." 1594 msgstr "" 1595 1596 msgid "Marketing (offers, promotions)" 1597 msgstr "" 1598 1599 msgid "Utility (order, account updates)" 1600 msgstr "" 1601 1602 msgid "No header" 1603 msgstr "" 1604 1605 msgid "Text" 1606 msgstr "" 1607 1608 msgid "Video" 1609 msgstr "" 1610 1611 msgid "Document" 1612 msgstr "" 1613 1614 msgid "Header text" 1615 msgstr "" 1616 1617 msgid "You will choose the actual image/video/file when you send a campaign." 1618 msgstr "" 1619 1620 msgid "Note:" 1621 msgstr "" 1622 1623 msgid "Utility templates are for transactional messages only (order, shipping, account). Promotional content will be rejected by Meta." 1624 msgstr "" 1625 1626 msgid "Insert a variable" 1627 msgstr "" 1628 1629 msgid "(filled in automatically at send time)" 1630 msgstr "" 1631 1632 msgid "Buttons" 1633 msgstr "" 1634 1635 msgid "(up to 10, optional)" 1636 msgstr "" 1637 1638 msgid "Add button" 1639 msgstr "" 1640 1641 msgid "Opt-out method (required for marketing)" 1642 msgstr "" 1643 1644 msgid "Footer text (recommended)" 1645 msgstr "" 1646 1647 msgid "Unsubscribe button" 1648 msgstr "" 1649 1650 msgid "Footer" 1651 msgstr "" 1652 1653 msgid "(small gray line, optional)" 1654 msgstr "" 1655 1656 msgid "Quick reply" 1657 msgstr "" 1658 1659 msgid "URL link" 1660 msgstr "" 1661 1662 msgid "Phone number" 1663 msgstr "" 1664 1665 msgid "Button text" 1666 msgstr "" 1667 1668 msgid "A URL or phone button needs a value." 1669 msgstr "" 1670 1671 msgid "Remove" 1672 msgstr "" 1673 1674 msgid "Two variables cannot be next to each other. Add some text between them (Meta rejects it otherwise)." 1675 msgstr "" 1676 1677 msgid "The message text must be between 5 and 1024 characters." 1678 msgstr "" 1679 1680 msgid "Meta rejected the template. %1$s (fallback: %2$s)" 1681 msgstr "" 1682 1683 msgid "Unknown error." 1684 msgstr "" 1685 1686 msgid "The template could not be submitted. %s" 1687 msgstr "" 1688 1689 msgid "This number is not on your marketing list, so there is no name to update." 1690 msgstr "" 1691 1692 msgid "Paste your API key (it starts with gk_) from the GoUltra Dashboard → Settings → API." 1693 msgstr "" 1694 1695 msgid "Campaign name (optional)" 1696 msgstr "" 1697 1698 msgid "e.g. Summer Sale - July" 1699 msgstr "" 1700 1701 msgid "A name to recognize this campaign in your history." 1702 msgstr "" 1703 1704 msgid "Upload / Media Library" 1705 msgstr "" 1706 1707 msgid "Image selected" 1708 msgstr "" 1709 1710 msgid "Or paste an image URL" 1711 msgstr "" 1712 1713 msgid "JPEG or PNG images only. Choose from your Media Library, pick a store product, or paste a URL." 1714 msgstr "" 1715 1716 msgid "Enter a valid WhatsApp number, including the country code." 1717 msgstr "" 1718 1719 msgid "Too many code requests. Please wait a while before trying again." 1720 msgstr "" 1721 1722 msgid "Your WhatsApp verification template is still being approved by Meta (usually a few minutes, sometimes longer). Please try again shortly - nothing else is needed from you." 1723 msgstr "" 1724 1725 msgid "Could not send the code. Please check the number and try again." 1726 msgstr "" 1727 1728 msgid "We sent a 6-digit code to %s on WhatsApp. Enter it below." 1729 msgstr "" 1730 1731 msgid "The code expired. Please request a new one." 1732 msgstr "" 1733 1734 msgid "Too many wrong attempts. Please request a new code." 1735 msgstr "" 1736 1737 msgid "Incorrect code. Attempts left: %d." 1738 msgstr "" 1739 1740 msgid "Your number is verified. It is now your store manager number and can be selected as a campaign recipient for your own tests." 1741 msgstr "" 1742 1743 msgid "Store manager" 1744 msgstr "" 1745 1746 msgid "Verify your number (recommended)" 1747 msgstr "" 1748 1749 msgid "Confirm your WhatsApp number with a code. A verified number becomes your store manager number, which you can select as a campaign recipient to test your own campaigns." 1750 msgstr "" 1751 1752 msgid "Verified" 1753 msgstr "" 1754 1755 msgid "Verify a different number" 1756 msgstr "" 1757 1758 msgid "Send code" 1759 msgstr "" 1760 1761 msgid "Verify" 1762 msgstr "" 1763 1764 msgid "Resend code" 1765 msgstr "" 1766 1767 msgid "Verifying..." 1768 msgstr "" 1769 1770 msgid "Enter your WhatsApp number first." 1771 msgstr "" 1772 1773 msgid "Enter the code you received." 1774 msgstr "" 1775 1776 msgid "Could not send the code." 1777 msgstr "" 1778 1779 msgid "Incorrect code." 1780 msgstr "" 1781 1782 msgid "Network error. Please try again." 1783 msgstr "" 1784 1785 msgid "Building a template for a product?" 1786 msgstr "" 1787 1788 msgid "Pick a store product and we set up the template for it: an image header plus a \"View product\" button that links to it. Then choose a language and submit for approval." 1789 msgstr "" 1790 1791 msgid "View product" 1792 msgstr "" 1793 1794 msgid "No products found." 1795 msgstr "" 1796 1797 msgid "You already have an approved or pending template of this type for this language. Create another one?" 1798 msgstr "" -
goultra-for-woocommerce/trunk/readme.txt
r3603449 r3617733 1 === GoUltra for WooCommerce ===1 === GoUltra for WooCommerce - WhatsApp Marketing, Notifications & Abandoned Cart === 2 2 Contributors: goultraai 3 Tags: woocommerce , whatsapp, abandoned cart, order notifications, shipping updates3 Tags: woocommerce-whatsapp, whatsapp-marketing, whatsapp-notification, abandoned-cart, whatsapp-api 4 4 Requires at least: 6.0 5 5 Tested up to: 7.0 6 6 Requires PHP: 7.4 7 Stable tag: 3. 8.27 Stable tag: 3.9.24 8 8 License: GPLv2 or later 9 9 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 11 11 WC tested up to: 10.8 12 12 13 Send WhatsApp order confirmations, shipping updates, and abandoned cart recovery messages to your WooCommerce customers viaGoUltra.13 WooCommerce WhatsApp notifications: automated order, shipping and abandoned-cart messages, plus opt-in marketing campaigns, powered by GoUltra. 14 14 15 15 == Description == 16 16 17 **GoUltra for WooCommerce** connects your online store to [GoUltra.AI](https://goultra.ai), a WhatsApp Business Platform, enabling you to send automated WhatsApp messages to your customers.17 **GoUltra for WooCommerce** brings WhatsApp into your WooCommerce store, managed entirely from WordPress. Compose and send opt-in WhatsApp marketing campaigns to the shoppers who chose to hear from you, and automatically send order confirmations, shipping and tracking updates, and consent-based abandoned-cart reminders, all through the WhatsApp Cloud API from Meta. A free WhatsApp chat button also lets visitors message your store in one tap, with no GoUltra account required. 18 18 19 = Features = 19 Build approved message templates in 20+ languages, preview each one exactly as it will look on WhatsApp, and run opt-in marketing campaigns, all without leaving your WordPress dashboard. Order and shipping updates are transactional and go out automatically for the WooCommerce statuses you choose. Marketing and abandoned-cart messages are sent only to customers who explicitly opt in at checkout, and you can search, export or unsubscribe your audience at any time. 20 20 21 * **Order Confirmations** - Automatically send a WhatsApp message with order details and product images when a customer completes a purchase. 22 * **Shipping Updates** - Notify customers when their order ships, including tracking links. Compatible with Advanced Shipment Tracking (AST). 23 * **Abandoned Cart Recovery (opt-in only)** - For shoppers who give explicit WhatsApp marketing permission at checkout, send a 3-step recovery sequence (nudge, reminder, discount coupon). Consent is re-checked before every message. 24 * **Admin Alerts** - Get instant WhatsApp notifications on your phone for every new order. 25 * **AI-Powered Support** - Sync your store policies so GoUltra's AI can automatically answer customer questions via WhatsApp. 26 * **Template Builder** - Create and manage WhatsApp message templates in 20+ languages directly from WordPress. 27 * **Consent-based** - Marketing messages are only sent to customers who explicitly opt in at checkout, and the plugin registers with WordPress's personal-data export and erase tools. 28 * **Webhook Logs** - View the last 500 API events with status codes for easy debugging. 29 * **HPOS Compatible** - Fully compatible with WooCommerce High-Performance Order Storage. 21 = Send WhatsApp marketing campaigns = 30 22 31 = How It Works = 23 Create a campaign from your opted-in shoppers, map your template variables, add a header image, and preview it before it sends. Send now or schedule for later, and follow every campaign in the history with pause and cancel. 32 24 33 1. Install the plugin and enter your GoUltra API key. 34 2. Configure which notifications to enable (order confirmations, shipping, abandoned cart, admin alerts). 35 3. Customize your WhatsApp message templates in any language. 36 4. The plugin sends lightweight payloads to GoUltra's servers - no heavy processing on your site. 37 5. GoUltra handles message delivery through the WhatsApp Business Platform (the WhatsApp Cloud API from Meta). 25 = Core workflows = 38 26 39 = Third-Party Service = 27 * **WhatsApp marketing campaigns** - Compose and send consent-based campaigns from WordPress through your connected GoUltra account, with a live preview, a test send, and send-now or schedule options. 28 * **Marketing audience** - Automatically add consenting shoppers to a searchable audience that you can export and unsubscribe from marketing. 29 * **Abandoned-cart recovery** - Run a configurable three-step reminder sequence only for shoppers who explicitly opted in to WhatsApp marketing. 30 * **Free WhatsApp chat button** - Add a one-tap chat button (floating widget, shortcode or product-page button) so visitors can message your store. Works with no GoUltra account, using a standard WhatsApp click-to-chat link. 31 * **Order confirmations and status updates** - Send the customer an approved WhatsApp template when an order is created or moves to a WooCommerce status you choose. 32 * **Shipping notifications** - Send shipping and tracking updates, including compatibility with Advanced Shipment Tracking (AST). 33 * **Multilingual templates** - Create and manage WhatsApp templates in 20+ languages, including ready-to-use starter content in major languages. 34 * **Admin alerts** - Send new-order alerts to an authorized store administrator. 35 * **Operational visibility** - Review message activity, API responses and recent webhook events. 40 36 41 This plugin connects to [GoUltra.AI](https://goultra.ai), a third-party SaaS service, to deliver WhatsApp messages. An active GoUltra account is required for the plugin to function. 37 = Built for WooCommerce operations = 38 39 * Compatible with WooCommerce High-Performance Order Storage (HPOS) and classic + block-based checkout. 40 * Processes outbound events asynchronously, so checkout never waits for the messaging API. 41 * Integrates with WordPress personal-data export and erase tools. 42 43 = Languages = 44 45 The plugin interface follows the WordPress site language and is available in English, Hebrew, Arabic, Spanish and French. Hebrew and Arabic include right-to-left layouts. The template builder includes starter content in 20+ languages. 46 47 = GoUltra account required = 48 49 This plugin connects to [GoUltra](https://goultra.ai), a third-party SaaS service, to deliver WhatsApp messages. An active GoUltra account is required to send WhatsApp messages. GoUltra subscription charges and Meta WhatsApp messaging charges are separate. 42 50 43 51 * [GoUltra Terms of Service](https://goultra.ai/terms) 44 52 * [GoUltra Privacy Policy](https://goultra.ai/privacy) 45 53 46 WhatsApp message delivery costs are billed by Meta (WhatsApp) based on conversation type and volume.54 Your store remains responsible for its consent text, privacy notices, audience eligibility and applicable legal requirements. 47 55 48 56 == Installation == 49 57 50 1. Upload the `goultra-for-woocommerce` folder to `/wp-content/plugins/` .51 2. Activate the plugin through the **Plugins** menu in WordPress.52 3. Go to **GoUltra →Settings** in the admin sidebar.53 4. Create a GoUltra account at [goultra.ai/register](https://goultra.ai/register) if you do n't have one.58 1. Upload the `goultra-for-woocommerce` folder to `/wp-content/plugins/`, or install it from the Plugins screen in WordPress. 59 2. Activate the plugin through the **Plugins** menu (WooCommerce must be installed and active). 60 3. Go to **GoUltra -> Settings** in the admin sidebar. 61 4. Create a GoUltra account at [goultra.ai/register](https://goultra.ai/register) if you do not have one. 54 62 5. Paste your API key and click **Connect**. 55 6. C onfigure your notification preferences and you're done!63 6. Choose which notifications to enable and which approved templates to use. 56 64 57 65 == Frequently Asked Questions == … … 59 67 = Do I need a GoUltra account? = 60 68 61 Yes. This plugin requires an active GoUltra account to send WhatsApp messages. You can create a free account at [goultra.ai/register](https://goultra.ai/register). 69 Yes, to send WhatsApp messages. Order, shipping, abandoned-cart and marketing messages all send through an active GoUltra account. You can create one at [goultra.ai/register](https://goultra.ai/register). The free WhatsApp chat button works without a GoUltra account. 70 71 = Can I send WhatsApp marketing campaigns from WordPress? = 72 73 Yes. You can compose and send opt-in WhatsApp marketing campaigns to your consenting audience directly from the GoUltra menu in WordPress, with a live preview and a test send. Campaigns go only to shoppers who explicitly opted in to WhatsApp marketing. 74 75 = Can I schedule a WhatsApp campaign? = 76 77 Yes. Choose send-now or pick a date and time to schedule a campaign, then follow it in the campaign history with pause and cancel. 78 79 = Can I preview a campaign before it sends? = 80 81 Yes. The composer shows a live WhatsApp-style preview of your template with your variables and header image filled in, and you can send a test message to your own verified number first. 82 83 = Do I need a GoUltra account to use the WhatsApp chat button? = 84 85 No. The WhatsApp chat button is a standard WhatsApp click-to-chat link (wa.me) to your own number. It works with no GoUltra account and sends no data to any service; the customer's own WhatsApp opens the conversation. 86 87 = Can I build my own WhatsApp message templates? = 88 89 Yes. The template builder lets you create order, shipping, admin, abandoned-cart and marketing templates in 20+ languages and submit them for Meta approval, with a required opt-out on marketing templates. 62 90 63 91 = How much does it cost? = 64 92 65 The WordPress plugin is free . GoUltra has affordable monthly plans. WhatsApp message costs are set by Meta and are typically very low for utility/service messages.93 The WordPress plugin is free to install. An active GoUltra account is required to send messages. GoUltra subscription charges and Meta WhatsApp messaging charges are separate. 66 94 67 = How does it handle GDPR and consent? =95 = Can I send marketing messages to every WooCommerce customer? = 68 96 69 Marketing messages (such as abandoned-cart recovery) require an explicit opt-in checkbox at checkout and are only sent to customers who consent. The plugin also registers with WordPress's personal-data export and erase tools. Your overall compliance still depends on your own store's privacy practices and policy. 97 No. Marketing and abandoned-cart messages are limited to contacts who have explicitly opted in to WhatsApp marketing at checkout. Order and shipping (utility) updates are transactional and are not marketing. Store owners remain responsible for their consent wording and legal obligations. 98 99 = Does the plugin slow down checkout? = 100 101 No. Outbound message events are queued asynchronously, so checkout does not wait for the GoUltra API to respond. 102 103 = How does it handle consent and personal data? = 104 105 Marketing messages (such as abandoned-cart recovery) require an explicit, unchecked-by-default opt-in at checkout and are sent only to customers who consent. Consent is re-checked before each marketing send, and unsubscribe stops marketing without affecting transactional order and shipping updates. The plugin also integrates with WordPress personal-data export and erase tools. 70 106 71 107 = Does this work with WooCommerce HPOS? = 72 108 73 Yes. The plugin is fully compatible with WooCommerce High-Performance Order Storage. 74 75 = What languages are supported? = 76 77 The template builder supports 20+ languages including English, Hebrew, Arabic, Russian, Spanish, French, German, Portuguese, Italian, Turkish, Japanese, Korean, Chinese, and more. 109 Yes. The plugin is fully compatible with HPOS and with both classic and block-based checkout. 78 110 79 111 == Screenshots == 80 112 81 1. Settings page - connect your GoUltra account. 82 2. Feature toggles - enable/disable notifications. 83 3. Template builder - create messages in any language. 84 4. Webhook logs - monitor API activity. 85 5. Checkout opt-in - WhatsApp consent checkbox. 113 1. Compose and schedule a WhatsApp marketing campaign, with a live preview and a test send. 114 2. A searchable marketing audience built from shoppers who actively opted in. 115 3. The template builder: marketing, order, shipping, admin and abandoned-cart message templates. 116 4. Active now: every active WooCommerce WhatsApp workflow in one place. 117 5. Map WooCommerce order statuses to approved WhatsApp templates. 118 6. Configure a three-step abandoned-cart recovery sequence. 119 7. Monitor messaging activity and recovered-cart revenue from WordPress. 120 8. Set up the free WhatsApp chat button, with no GoUltra account needed. 86 121 87 122 == Changelog == 88 123 89 = 3.8.2 = 90 * Marketing List: full right-to-left layout for Hebrew and Arabic and a redesigned screen (summary cards, a cleaner table and clearer actions) matching the rest of the plugin. 91 * New list settings: choose whether the contact name sent to your GoUltra list is the full name, the first name only or the last name only, and whether the email address is included. Applies to shoppers added from now on. 92 * Settings: a new "Active now" card summarizes exactly which notifications are currently enabled (customer messages per status, admin alerts, abandoned-cart steps, quiet hours and the marketing list). 93 * The test-send bar is now clearly labeled as optional. 124 The three most recent releases are below. Older entries are in changelog.txt. 94 125 95 = 3.8.1 = 96 * Clarified wording on the listing: describes the WhatsApp Business Platform (the WhatsApp Cloud API from Meta) accurately, and states that abandoned-cart recovery requires an explicit marketing opt-in at checkout. 126 = 3.9.24 = 127 * New: a free WhatsApp chat button (floating widget, [goultra_whatsapp_button] shortcode, and an optional product-page button) so visitors can message your store in one tap. It uses a standard WhatsApp click-to-chat link and needs no GoUltra account. 128 * The listing and in-plugin wording now lead with WhatsApp marketing campaigns. 129 * Refreshed screenshots. 97 130 98 = 3.8.0 = 99 * New: a Marketing List screen (GoUltra menu). Shoppers who give explicit WhatsApp marketing consent at checkout are now collected into a warm subscriber list you can search, export to CSV, and unsubscribe from. This list is your per-store marketing audience for campaigns. 100 * Consenting shoppers are recorded automatically: buyers who tick the marketing opt-in, and shoppers who consent but abandon their cart. Everything runs in the background so checkout is never slowed. 101 * Unsubscribe removes a contact from marketing only. Order and shipping (utility) updates still reach them, in line with WhatsApp and consent rules. 102 * Privacy: a WordPress "erase personal data" request now also removes the contact from the GoUltra marketing list, not just the local record. 131 = 3.9.23 = 132 * Clearer listing description, and a full pre-release check: sending safety, security, template and campaign flows, translations and WordPress.org compliance were all verified. 103 133 104 = 3.7.16 = 105 * Fix: order and shipping WhatsApp messages now go out within seconds of the order. The queued send is kicked immediately by a non-blocking loopback instead of waiting for the next traffic-dependent wp-cron tick (which could take several minutes on a quiet store). 106 107 = 3.7.15 = 108 * Fixed: the WhatsApp template chosen for an order status or admin alert could be lost after a template sync, so the notification stayed enabled but silently did not send and you had to re-select it. Template ids are now kept stable across syncs, so your saved selections persist. 109 * Fixed: admin notifications could be silently skipped when no specific template was mapped. Admin alerts now fire on the toggle alone (the backend picks the admin template), consistent with customer notifications. 110 * Added a warning on the Settings screen when a notification is enabled but you have no approved WhatsApp templates yet, so it is clear why messages are not sending (create and approve a template in the Templates tab). 111 * The connection now shows a clear message when the GoUltra subscription is not active, instead of a raw error code. 112 * Security and compliance hardening: the REST webhook secret is accepted only via the Authorization header (no longer as a URL query parameter), plus minor WordPress.org Plugin Check cleanups. 113 * Updated the API key field hint to match the current GoUltra key format. 114 115 = 3.7.13 = 116 * Fixed the submission error "Your plugin and author URIs are the same": the Plugin URI now points to the plugin's WordPress.org directory page and the Author URI to the author site, so the two headers are distinct as required. 117 118 = 3.7.12 = 119 * WordPress.org review compliance: the admin CSS and JavaScript are now enqueued instead of printed inline (no inline style or script tags). Removed the "Powered by GoUltra" line from the message footers, so it is no longer added to customer messages by default. Added the "Requires Plugins: woocommerce" header and corrected the Plugin URI. 120 121 = 3.7.11 = 122 * Fixed: the Templates screen could reload itself a few seconds after opening (the background status sync refreshed the page), which sent you back to the start if you had already begun creating a template. The status sync now runs quietly in the background and never reloads the page; use the "Sync Status" button for an immediate refresh. 123 * Fixed: in a right-to-left admin (Arabic / Hebrew) the back arrow in the WhatsApp template preview pointed the wrong way; it now points correctly. 124 * Hebrew starter templates now use the proper gershayim character in the "total" abbreviation. 125 126 = 3.7.10 = 127 * Made the plugin display name in readme.txt match the plugin header exactly (WordPress.org submission consistency). No functional change. 128 129 = 3.7.9 = 130 * WordPress.org readiness + privacy: removed all remote Google Fonts from the admin screens (the UI now uses your system font stack), so the plugin makes no third-party requests for assets. This is faster, fully private (no data sent to Google), and resolves a common GDPR concern. 131 * Code-quality / Plugin Check hardening: tightened output escaping in the logs and templates screens, added input unslashing on a couple of admin save paths, gated internal debug logging behind WP_DEBUG, sanitize incoming template data on write (defense in depth), and added capability checks directly on the admin page renderers. No behavior change for store owners. 132 133 = 3.7.8 = 134 * Security hardening (from an adversarial penetration-test style review of the unauthenticated attack surface): the public abandoned-cart capture endpoint now rejects empty or zero-value carts, reducing automated abuse. Added defense-in-depth capability checks on the admin page renderers and extra output escaping in the Templates wizard. No vulnerability that could expose or compromise the host site was found; these are preventative hardening measures. 135 136 = 3.7.7 = 137 * Abandoned cart compliance (GDPR + ePrivacy): the plugin now stores a shopper's contact details for abandoned-cart recovery ONLY when they give marketing consent at checkout. Without consent, no personal data is kept for marketing. Consent is also re-checked at send time, so withdrawing consent stops any pending messages. 138 * Quiet hours now actually work: abandoned-cart marketing messages that would fire inside your quiet window are automatically rescheduled to when the window ends (transactional order/shipping messages are never delayed). 139 * GDPR data-subject tools: the plugin now integrates with WordPress Tools > Export/Erase Personal Data, so a customer's stored abandoned-cart data can be exported or erased on request, and it adds suggested privacy-policy text. 140 * All abandoned-cart logic verified end to end (consent gating, scheduling, sending, coupon generation, kill switch, data export/erase). 141 142 = 3.7.6 = 143 * Security hardening: the public store-info endpoint no longer exposes the WooCommerce or plugin version (reduces version fingerprinting). The backend still receives the plugin version via the authenticated request header. 144 145 = 3.7.5 = 146 * Hardening (from a full code + runtime audit): fixed a case where the delivery-status webhook could fail on PHP 7.4 (it used a PHP 8 only function); the plugin is now compatible with PHP 7.4 and up again. 147 * Performance: the analytics table is no longer re-created on every analytics write (one-time, gated), and the store-policies / status-template-map options are no longer autoloaded on every page request. 148 * Robustness: the abandoned-cart kill switch now catches all error types when cancelling scheduled actions, so it can never interrupt order processing. 149 * Audited: zero blocking HTTP on checkout (all sends are async), full HPOS compatibility, all admin AJAX protected by nonce + capability, all SQL uses prepared statements, REST endpoints use timing-safe secret checks. No customer-facing errors in testing. 150 151 = 3.7.4 = 152 * Analytics: the Analytics dashboard is now fully translated (Hebrew, Arabic, French, Spanish) and renders in right-to-left for Arabic and Hebrew (KPI cards, the stats row, and the recent-activity table all flip correctly). The month name now follows your WordPress language too. 153 154 = 3.7.3 = 155 * Templates: the built-in starter templates are now translated into 21 major world languages (English, Hebrew, Arabic, Spanish, French, German, Italian, Portuguese, Russian, Turkish, Dutch, Polish, Ukrainian, Indonesian, Malay, Vietnamese, Thai, Japanese, Korean, Chinese, Hindi). Choosing a template language now shows ready-to-use content in that language instead of English. Other languages still fall back to English. 156 * Language: removed the in-plugin interface-language switcher. The admin now simply follows your WordPress language (the cleaner, standard approach). This resolves the admin layout/right-to-left issues that the manual switcher could cause. To use the plugin in another language, set your WordPress language under Settings > General. 157 * Admin notifications: status toggles and template pickers are now disabled until an admin WhatsApp number is entered (you cannot enable an admin alert without a destination number). 158 * UX: the bottom test-send bar is clearer (neutral phone placeholder, tooltips, and a clearer "awaiting approval" label for templates pending Meta approval). 159 * RTL: fixed the "Back" arrow direction in the Templates wizard for Arabic and Hebrew (now points the correct way). 160 161 = 3.7.2 = 162 * Fix (critical): switching the GoUltra interface language to a non-default language (e.g. Arabic or Hebrew) could make GoUltra admin pages return HTTP 500. The locale filter no longer calls a translation function while WordPress is determining the locale (which caused infinite recursion). GoUltra admin pages now load correctly in every language. 163 * Text: removed the long-dash character from all plugin text for a cleaner, consistent style. 164 165 = 3.7.1 = 166 * Translations: the Templates screen is now fully localized (Hebrew, Arabic, French, Spanish) - message types, the template wizard, the custom-template editor, and all status messages. Arabic and Hebrew render in polished right-to-left. 167 * Convenience: connecting your API key now automatically pulls your existing approved templates in one step - no need to open the Templates screen and click Sync. The connect button confirms how many templates were imported. 168 169 = 3.7.0 = 170 * Translations: the plugin admin is now translatable and ships with Hebrew, Arabic, French and Spanish (English default; unsupported languages fall back to English). It auto-detects your WordPress language, and a new "GoUltra Interface Language" selector lets you force a specific language for the GoUltra screens. 171 * Right-to-left: full, polished RTL layout for Arabic and Hebrew on the Settings screen. 172 * (First i18n phase - Settings screen. Templates/Analytics screens are being localized next.) 173 174 = 3.6.0 = 175 * Consent: separated MARKETING from UTILITY messaging (Meta + GDPR best practice). Utility messages (order confirmation, shipping) are transactional and are now sent automatically to anyone who provides a phone - no opt-in required. Marketing messages (offers, abandoned-cart recovery) now require an EXPLICIT opt-in: an unchecked-by-default checkbox the customer must actively tick at checkout (classic + block checkout). Abandoned-cart recovery is sent only to customers who opted in to marketing. 176 * Removed the old microcopy/checkbox opt-in toggle (superseded by the consent separation above). 177 178 = 3.5.3 = 179 * UX: the template pickers (Order Status + Admin notifications) are now grouped by template type with a clear "Language · Style - preview" label, so multiple approved templates are easy to tell apart. Removed the confusing inline checkmark and fixed right-to-left rendering. 180 181 = 3.5.2 = 182 * Branding: the GoUltra logo now appears in the Settings, Templates, and Analytics page headers. 183 * Fix: corrected the API key field placeholder to match the real key format (sk_live_…). 184 185 = 3.5.1 = 186 * Block checkout: added a native WhatsApp opt-in checkbox (WooCommerce Additional Checkout Fields API) for "checkbox" opt-in mode, saved to the order. The block checkout is the default since WooCommerce 8.3, where the classic opt-in hooks do not fire. Microcopy mode keeps working via implicit consent. 187 188 = 3.5.0 = 189 * Performance: all outbound WhatsApp/API calls on order events now run as Action Scheduler async jobs, so checkout and order status changes are never blocked or slowed by the GoUltra API (even if it is slow or unreachable). 190 * Performance: removed redundant per-request database queries (schema check, scheduler check, analytics table check are now gated). 191 * Fix: duplicate order-status hooks no longer cause duplicate sends (per-status + per-order de-duplication). 192 * Fix: shipping updates now normalise the customer phone to E.164 (local-format numbers were sent unnormalised). 193 * Fix: consistent opt-in handling across order confirmation and shipping. 194 * Fix: product catalog sync now returns the correct total count and avoids a heavy full-catalog query. 195 * Fix: corrected a PHP notice on the settings page (status → template option list). 196 * Fix: PHP 8.x hardening (null-safe phone normalisation and string handling). 197 * Fix: deactivation/uninstall now unschedule the correct Action Scheduler hooks; uninstall also removes the analytics table. 198 * Compatibility: declared Cart & Checkout Blocks compatibility; ghost-capture now works on the block checkout; abandoned-cart coupons now use the WC_Coupon CRUD object. Tested up to WooCommerce 10.8 and WordPress 7.0. 199 200 = 1.0.0 = 201 * Initial release. 202 * Order confirmation notifications. 203 * Shipping update notifications with tracking. 204 * Abandoned cart recovery (3-step sequence). 205 * Admin new order alerts. 206 * Template builder with 20+ languages. 207 * Store policies sync for AI support. 208 * Webhook logs viewer. 209 * HPOS compatibility. 210 * GDPR-compliant checkout opt-in. 134 = 3.9.22 = 135 * Important safety fix: after a campaign is sent, the composer now clears itself (template, recipients, name, image), so the same campaign can no longer be sent again by accident. (It was already protected on the service side by a two-minute lock and duplicate detection; this closes the on-screen gap.) 136 * Campaign history table columns now line up under their headers in Hebrew and Arabic. 137 * The self-test tip under the recipient list now reads correctly right to left. 211 138 212 139 == Upgrade Notice == 213 140 214 = 3. 7.7=215 Stronger EU compliance for abandoned-cart recovery: personal data is stored only with marketing consent, quiet hours are now enforced for marketing messages, and WordPress export/erase personal-data tools are supported. Recommended for all EU stores.141 = 3.9.24 = 142 Adds a free WhatsApp chat button (no account needed) and leads with WhatsApp marketing campaigns. Open GoUltra to set it up. 216 143 217 = 3. 7.5=218 S tability and performance hardening from a full audit: PHP 7.4 webhook fix, fewer per-request queries, and verified safe for live stores (async sends, HPOS, secure AJAX/REST). Recommended.144 = 3.9.13 = 145 Sending-safety hardening for campaigns and abandoned cart: no accidental double sends, a clear notice if your API key stops working, and more accurate send counts. 219 146 220 = 3. 7.4=221 The Analytics dashboard is now translated (Hebrew, Arabic, French, Spanish) with full right-to-left support.147 = 3.9.9 = 148 Build a marketing template for a specific product: pick the product and get an image header and a "View product" button automatically. 222 149 223 = 3. 7.3=224 Built-in templates are now translated into 21 languages, admin alerts require an admin number, and the admin interface follows your WordPress language (the manual language switcher was removed to prevent layout issues).150 = 3.9.8 = 151 Verify your admin WhatsApp number with a one-time code, and use that verified store manager number to test your own campaigns. 225 152 226 = 3.7.2 = 227 Important fix: switching the GoUltra interface language to Arabic or Hebrew could crash GoUltra admin pages with HTTP 500. This release fixes that. Recommended for everyone using a non-English interface language. 228 229 = 3.7.1 = 230 The Templates screen is now fully translated (Hebrew, Arabic, French, Spanish) with right-to-left support, and connecting your API key now auto-syncs your existing templates instantly. 231 232 = 3.7.0 = 233 The admin is now available in Hebrew, Arabic, French and Spanish with full right-to-left support for Arabic/Hebrew. Set the language under GoUltra → Settings → General. 234 235 = 3.6.0 = 236 Important consent change: order/shipping updates are now sent automatically (transactional), while marketing (offers, abandoned cart) requires an explicit checkout opt-in checkbox. Review your checkout opt-in text under GoUltra → Settings. 237 238 = 3.5.3 = 239 Clearer, grouped template pickers so you can tell approved templates apart at a glance. 240 241 = 3.5.2 = 242 Adds the GoUltra logo to the admin headers and fixes the API key placeholder format. 243 244 = 3.5.1 = 245 Adds a native opt-in checkbox to the block-based checkout (default since WooCommerce 8.3). Recommended for all stores using the Checkout block. 246 247 = 3.5.0 = 248 Performance and reliability update: WhatsApp sends no longer block checkout, plus latest WooCommerce (10.8) / WordPress (7.0) compatibility and several bug fixes. 249 250 = 1.0.0 = 251 Initial release of GoUltra for WooCommerce. 153 = 3.9.0 = 154 Send consent-based WhatsApp marketing campaigns to your opted-in audience directly from WordPress, with a live preview, test send and scheduling. Includes a 21-language marketing template builder. -
goultra-for-woocommerce/trunk/templates/admin/settings-page.php
r3603449 r3617733 9 9 10 10 $connected = (bool) get_option( GOULTRA_WC_OPTION_PREFIX . 'connected', false ); 11 $api_key = get_option( GOULTRA_WC_OPTION_PREFIX . 'api_key', '' ); 11 // NB: the stored API key is intentionally NOT read into a variable that reaches the 12 // page. The connection field never pre-fills it (see the API Key input below), so the 13 // secret never appears in the rendered HTML. 12 14 $connected_phone = get_option( GOULTRA_WC_OPTION_PREFIX . 'connected_phone', '' ); 13 15 $business_name = get_option( GOULTRA_WC_OPTION_PREFIX . 'business_name', '' ); … … 49 51 <div><h1>GoUltra for WooCommerce <span class="gu-hdr-ver">v<?php echo esc_html( GOULTRA_WC_VERSION ); ?></span></h1></div> 50 52 <div class="gu-hdr-links"> 51 <a href="<?php echo esc_url( admin_url('admin.php?page=goultra-analytics') ); ?>"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/></svg> <?php esc_html_e( 'Analytics', 'goultra-for-woocommerce' ); ?></a>52 53 <a href="<?php echo esc_url( $templates_page_url ); ?>"><?php esc_html_e( 'Templates', 'goultra-for-woocommerce' ); ?></a> 54 <a href="<?php echo esc_url( admin_url('admin.php?page=goultra-subscribers') ); ?>"><?php esc_html_e( 'Marketing List', 'goultra-for-woocommerce' ); ?></a> 55 <a href="<?php echo esc_url( admin_url('admin.php?page=goultra-campaigns') ); ?>"><?php esc_html_e( 'Campaigns', 'goultra-for-woocommerce' ); ?></a> 56 <a href="<?php echo esc_url( admin_url('admin.php?page=goultra-analytics') ); ?>"><?php esc_html_e( 'Analytics', 'goultra-for-woocommerce' ); ?></a> 57 <a href="<?php echo esc_url( admin_url('admin.php?page=goultra-logs') ); ?>"><?php esc_html_e( 'Logs', 'goultra-for-woocommerce' ); ?></a> 53 58 <a href="https://goultra.ai/docs/woocommerce" target="_blank"><?php esc_html_e( 'Docs', 'goultra-for-woocommerce' ); ?></a> 54 59 <a href="https://goultra.ai/support" target="_blank"><?php esc_html_e( 'Support', 'goultra-for-woocommerce' ); ?></a> … … 83 88 <label><?php esc_html_e( 'API Key', 'goultra-for-woocommerce' ); ?></label> 84 89 <div class="gu-ig"> 85 <input type="password" id="gu-api-key" placeholder="key_xxxxxxxxxxxxxxxxxxxx" value="<?php echo esc_attr($api_key); ?>" autocomplete="off" /> 90 <?php // Do NOT echo any stored key into the page HTML: this field is only for ENTERING a key while disconnected, and keeping the value out of the DOM means it can never leak via a screenshot, a shared screen, or "view source". ?> 91 <input type="password" id="gu-api-key" placeholder="gk_live_xxxxxxxxxxxxxxxxxxxx" value="" autocomplete="off" /> 86 92 <button type="button" class="gu-btn gu-btn-p" id="gu-conn-btn"><?php esc_html_e( 'Connect', 'goultra-for-woocommerce' ); ?></button> 87 93 </div> 88 <div class="gu-desc"><?php esc_html_e( 'Paste your API key fromGoUltra Dashboard → Settings → API.', 'goultra-for-woocommerce' ); ?></div>94 <div class="gu-desc"><?php esc_html_e( 'Paste your API key (it starts with gk_) from the GoUltra Dashboard → Settings → API.', 'goultra-for-woocommerce' ); ?></div> 89 95 </div> 90 96 <?php endif; ?> … … 113 119 // Legacy fallback (no mapping saved yet): the old toggles govern. 114 120 if ( empty( $gu_live_map ) ) { 115 if ( 'yes' === get_option( GOULTRA_WC_OPTION_PREFIX . 'enable_order_confirm', ' yes' ) ) {121 if ( 'yes' === get_option( GOULTRA_WC_OPTION_PREFIX . 'enable_order_confirm', 'no' ) ) { 116 122 $gu_live_cust[] = $gu_live_lbl( get_option( GOULTRA_WC_OPTION_PREFIX . 'order_confirm_trigger', 'processing' ) ); 117 123 } 118 if ( 'yes' === get_option( GOULTRA_WC_OPTION_PREFIX . 'enable_admin_notify', ' yes' ) ) {124 if ( 'yes' === get_option( GOULTRA_WC_OPTION_PREFIX . 'enable_admin_notify', 'no' ) ) { 119 125 $gu_live_admin[] = __( 'New order', 'goultra-for-woocommerce' ); 120 126 } 121 127 } 122 $gu_live_ac = 'yes' === get_option( GOULTRA_WC_OPTION_PREFIX . 'enable_abandoned_cart', ' yes' );128 $gu_live_ac = 'yes' === get_option( GOULTRA_WC_OPTION_PREFIX . 'enable_abandoned_cart', 'no' ); 123 129 $gu_live_steps = array(); 124 130 foreach ( array( 1, 2, 3 ) as $gu_i ) { … … 179 185 180 186 <form id="gu-settings-form"> 187 188 <!-- FREE WHATSAPP CHAT BUTTON (front-end, no GoUltra account needed) --> 189 <div class="gu-card"> 190 <div class="gu-card-h"><h2><span class="gu-ico" style="background:#ECFDF5;"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#0F172A" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"/></svg></span> <?php esc_html_e( 'WhatsApp Chat Button', 'goultra-for-woocommerce' ); ?></h2><p><?php esc_html_e( 'Add a free one-tap WhatsApp chat button to your store. It works with no GoUltra account: it opens a WhatsApp chat to your number, using a standard click-to-chat link.', 'goultra-for-woocommerce' ); ?></p></div> 191 <div class="gu-card-b"> 192 <div class="gu-tog"> 193 <label class="gu-tog-sw"><input type="checkbox" name="chat_button_enabled" value="yes" data-toggle="cb-sub" <?php checked($opt('chat_button_enabled','no'),'yes'); ?> /><span class="gu-tog-sl"></span></label> 194 <div class="gu-tog-info"><strong><?php esc_html_e( 'Show the floating chat button', 'goultra-for-woocommerce' ); ?></strong><span><?php esc_html_e( 'A round WhatsApp button in the corner of your storefront.', 'goultra-for-woocommerce' ); ?></span></div> 195 </div> 196 <div class="gu-sub" id="cb-sub"> 197 <div class="gu-field"> 198 <label><?php esc_html_e( 'WhatsApp number', 'goultra-for-woocommerce' ); ?></label> 199 <input type="text" name="chat_button_phone" dir="ltr" value="<?php echo esc_attr($opt('chat_button_phone','')); ?>" placeholder="+1 555 018 2244" /> 200 <div class="gu-desc"><?php esc_html_e( 'Include the country code. This is the number that will receive the chats.', 'goultra-for-woocommerce' ); ?></div> 201 </div> 202 <div class="gu-field"> 203 <label><?php esc_html_e( 'Pre-filled message', 'goultra-for-woocommerce' ); ?></label> 204 <input type="text" name="chat_button_message" value="<?php echo esc_attr($opt('chat_button_message','')); ?>" placeholder="<?php esc_attr_e( 'Hi! I have a question about your store.', 'goultra-for-woocommerce' ); ?>" /> 205 <div class="gu-desc"><?php esc_html_e( 'The message the customer starts with. Leave empty for none.', 'goultra-for-woocommerce' ); ?></div> 206 </div> 207 <div class="gu-field"> 208 <label><?php esc_html_e( 'Button label', 'goultra-for-woocommerce' ); ?></label> 209 <input type="text" name="chat_button_label" value="<?php echo esc_attr($opt('chat_button_label','')); ?>" placeholder="<?php esc_attr_e( 'Chat with us', 'goultra-for-woocommerce' ); ?>" /> 210 </div> 211 <div class="gu-field-row"> 212 <div class="gu-field gu-field-sm"> 213 <label><?php esc_html_e( 'Position', 'goultra-for-woocommerce' ); ?></label> 214 <select name="chat_button_position"> 215 <option value="bottom-right" <?php selected($opt('chat_button_position','bottom-right'),'bottom-right'); ?>><?php esc_html_e( 'Bottom right', 'goultra-for-woocommerce' ); ?></option> 216 <option value="bottom-left" <?php selected($opt('chat_button_position','bottom-right'),'bottom-left'); ?>><?php esc_html_e( 'Bottom left', 'goultra-for-woocommerce' ); ?></option> 217 </select> 218 </div> 219 <div class="gu-field gu-field-sm"> 220 <label><?php esc_html_e( 'Show on', 'goultra-for-woocommerce' ); ?></label> 221 <select name="chat_button_visibility"> 222 <option value="all" <?php selected($opt('chat_button_visibility','all'),'all'); ?>><?php esc_html_e( 'All pages', 'goultra-for-woocommerce' ); ?></option> 223 <option value="shop_product" <?php selected($opt('chat_button_visibility','all'),'shop_product'); ?>><?php esc_html_e( 'Shop and product pages only', 'goultra-for-woocommerce' ); ?></option> 224 </select> 225 </div> 226 </div> 227 <div class="gu-tog" style="margin-top:6px;"> 228 <label class="gu-tog-sw"><input type="checkbox" name="chat_button_product" value="yes" <?php checked($opt('chat_button_product','no'),'yes'); ?> /><span class="gu-tog-sl"></span></label> 229 <div class="gu-tog-info"><strong><?php esc_html_e( 'Add a button on product pages', 'goultra-for-woocommerce' ); ?></strong><span><?php esc_html_e( 'Shows an "Ask about this product" WhatsApp button under Add to cart, with the product name filled in.', 'goultra-for-woocommerce' ); ?></span></div> 230 </div> 231 <div class="gu-desc" style="margin-top:10px;"><?php echo esc_html__( 'Tip: you can also place a button anywhere with the shortcode', 'goultra-for-woocommerce' ) . ' '; ?><code>[goultra_whatsapp_button]</code></div> 232 </div> 233 </div> 234 </div> 181 235 182 236 <!-- ORDER STATUS → WHATSAPP TEMPLATE MAPPING --> … … 282 336 </div> 283 337 </div> 338 339 <?php 340 $gu_av_num = (string) get_option( GOULTRA_WC_OPTION_PREFIX . 'admin_phone_verified', '' ); 341 $gu_av_parts = explode( ',', (string) $opt('admin_phone') ); 342 $gu_av_first = trim( (string) ( $gu_av_parts[0] ?? '' ) ); 343 ?> 344 <div class="gu-av" id="gu-av"> 345 <div class="gu-av-head"> 346 <span class="gu-av-ico" aria-hidden="true"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#0369a1" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/><path d="M9 12l2 2 4-4"/></svg></span> 347 <div> 348 <div class="gu-av-title"><?php esc_html_e( 'Verify your number (recommended)', 'goultra-for-woocommerce' ); ?></div> 349 <div class="gu-desc"><?php esc_html_e( 'Confirm your WhatsApp number with a code. A verified number becomes your store manager number, which you can select as a campaign recipient to test your own campaigns.', 'goultra-for-woocommerce' ); ?></div> 350 </div> 351 </div> 352 <div class="gu-av-done" id="gu-av-done" style="<?php echo $gu_av_num ? '' : 'display:none;'; ?>"> 353 <span class="gu-av-badge">✓ <?php esc_html_e( 'Verified', 'goultra-for-woocommerce' ); ?></span> 354 <bdi dir="ltr" id="gu-av-num"><?php echo $gu_av_num ? esc_html( '+' . preg_replace( '/\D/', '', $gu_av_num ) ) : ''; ?></bdi> 355 <button type="button" class="gu-btn gu-btn-o gu-camp-sm" id="gu-av-again"><?php esc_html_e( 'Verify a different number', 'goultra-for-woocommerce' ); ?></button> 356 </div> 357 <div class="gu-av-start" id="gu-av-start" style="<?php echo $gu_av_num ? 'display:none;' : ''; ?>"> 358 <input type="tel" id="gu-av-phone" dir="ltr" placeholder="+1 (555) 123-4567" value="<?php echo esc_attr( $gu_av_first ); ?>" /> 359 <button type="button" class="gu-btn gu-btn-cy gu-camp-sm" id="gu-av-send"><?php esc_html_e( 'Send code', 'goultra-for-woocommerce' ); ?></button> 360 </div> 361 <div class="gu-av-codewrap" id="gu-av-codewrap" style="display:none;"> 362 <input type="text" id="gu-av-code" dir="ltr" inputmode="numeric" maxlength="6" placeholder="123456" /> 363 <button type="button" class="gu-btn gu-btn-cy gu-camp-sm" id="gu-av-verify"><?php esc_html_e( 'Verify', 'goultra-for-woocommerce' ); ?></button> 364 <a href="#" id="gu-av-resend"><?php esc_html_e( 'Resend code', 'goultra-for-woocommerce' ); ?></a> 365 </div> 366 <div class="gu-av-msg" id="gu-av-msg" role="status" aria-live="polite"></div> 367 </div> 284 368 <p id="gu-admin-need-phone" style="display:none;font-size:12px;color:#b45309;background:#fffbeb;border:1px solid #fde68a;border-radius:8px;padding:9px 12px;margin:0 0 12px;"><?php esc_html_e( 'Enter an admin WhatsApp number above to enable these notifications.', 'goultra-for-woocommerce' ); ?></p> 285 369 <div id="gu-admin-rows"> … … 312 396 <div class="gu-card-b"> 313 397 <div class="gu-tog" id="tog-ac"> 314 <label class="gu-tog-sw"><input type="checkbox" name="enable_abandoned_cart" value="yes" data-toggle="ac-sub" <?php checked($opt('enable_abandoned_cart',' yes'),'yes'); ?> /><span class="gu-tog-sl"></span></label>398 <label class="gu-tog-sw"><input type="checkbox" name="enable_abandoned_cart" value="yes" data-toggle="ac-sub" <?php checked($opt('enable_abandoned_cart','no'),'yes'); ?> /><span class="gu-tog-sl"></span></label> 315 399 <div class="gu-tog-info"><strong><?php esc_html_e( 'Enable Abandoned Cart Recovery', 'goultra-for-woocommerce' ); ?></strong><span><?php esc_html_e( 'Automatic recovery messages when customers abandon checkout', 'goultra-for-woocommerce' ); ?></span></div> 316 400 </div> … … 478 562 <div class="gu-card"> 479 563 <div class="gu-card-h"> 480 <h2><span class="gu-ico" style="background:#F0FDFA;"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#0F172A" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg></span> <?php esc_html_e( 'REST API & Webhooks', 'goultra-for-woocommerce' ); ?></h2> 481 <p><?php esc_html_e( 'Configure these in your GoUltra dashboard → WooCommerce settings', 'goultra-for-woocommerce' ); ?></p> 482 </div> 483 <div class="gu-card-b" style="display:flex;flex-direction:column;gap:16px;"> 484 485 <div style="background:#f8fafc;border:1px solid #e2e8f0;border-radius:10px;padding:16px;"> 486 <div style="font-size:11px;font-weight:600;text-transform:uppercase;color:#64748b;margin-bottom:6px;"><?php esc_html_e( 'Webhook Secret (Bearer Token)', 'goultra-for-woocommerce' ); ?></div> 487 <div style="display:flex;gap:8px;align-items:center;"> 488 <code id="gu-wh-secret" style="flex:1;background:#fff;border:1px solid #cbd5e1;border-radius:6px;padding:8px 12px;font-size:12px;font-family:monospace;word-break:break-all;" dir="ltr"><?php echo esc_html( $webhook_secret ); ?></code> 489 <button type="button" data-copy="<?php esc_attr_e( 'Copy', 'goultra-for-woocommerce' ); ?>" data-copied="<?php esc_attr_e( '✓ Copied!', 'goultra-for-woocommerce' ); ?>" onclick="navigator.clipboard.writeText(document.getElementById('gu-wh-secret').textContent).then(()=>{var t=this;t.textContent=t.dataset.copied;setTimeout(()=>t.textContent=t.dataset.copy,1500)})" style="white-space:nowrap;padding:8px 14px;background:#0f172a;color:#fff;border:none;border-radius:8px;cursor:pointer;font-size:12px;font-weight:600;"><?php esc_html_e( 'Copy', 'goultra-for-woocommerce' ); ?></button> 490 </div> 491 <div style="font-size:11px;color:#94a3b8;margin-top:6px;"><?php echo wp_kses( __( 'GoUltra backend sends this as <code>Authorization: Bearer {secret}</code>', 'goultra-for-woocommerce' ), array( 'code' => array() ) ); ?></div> 492 </div> 493 494 <div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;"> 495 <div style="background:#f0fdf4;border:1px solid #bbf7d0;border-radius:10px;padding:14px;"> 496 <div style="font-size:11px;font-weight:600;color:#16a34a;margin-bottom:6px;"><?php esc_html_e( 'Delivery Status Webhook', 'goultra-for-woocommerce' ); ?></div> 497 <code style="font-size:11px;word-break:break-all;color:#166534;" dir="ltr"><?php echo esc_url( $base_url . '/webhook' ); ?></code> 498 <div style="font-size:11px;color:#86efac;margin-top:4px;"><?php esc_html_e( 'POST - receives sent/delivered/read/failed events → adds order notes', 'goultra-for-woocommerce' ); ?></div> 499 </div> 500 <div style="background:#eff6ff;border:1px solid #bfdbfe;border-radius:10px;padding:14px;"> 501 <div style="font-size:11px;font-weight:600;color:#2563eb;margin-bottom:6px;"><?php esc_html_e( 'Product Catalog Sync', 'goultra-for-woocommerce' ); ?></div> 502 <code style="font-size:11px;word-break:break-all;color:#1e3a8a;" dir="ltr"><?php echo esc_url( $base_url . '/sync' ); ?></code> 503 <div style="font-size:11px;color:#93c5fd;margin-top:4px;"><?php esc_html_e( 'GET - paginated product feed for GoUltra AI recommendations', 'goultra-for-woocommerce' ); ?></div> 504 </div> 505 </div> 506 507 <div style="background:#fef9c3;border:1px solid #fde68a;border-radius:10px;padding:12px 14px;font-size:12px;color:#92400e;"> 508 <?php echo wp_kses( __( '<strong>Setup in GoUltra:</strong> Dashboard → Integrations → WooCommerce → paste the secret + webhook URL above. GoUltra will POST delivery updates automatically from Meta webhooks.', 'goultra-for-woocommerce' ), array( 'strong' => array() ) ); ?> 509 </div> 564 <h2><span class="gu-ico" style="background:#F0FDFA;"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#0F172A" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg></span> <?php esc_html_e( 'Delivery receipts and product sync', 'goultra-for-woocommerce' ); ?></h2> 565 <p><?php esc_html_e( 'These work automatically once your store is connected. There is nothing to set up.', 'goultra-for-woocommerce' ); ?></p> 566 </div> 567 <div class="gu-card-b" style="display:flex;flex-direction:column;gap:14px;"> 568 569 <div style="display:flex;gap:10px;align-items:flex-start;background:#f0fdf4;border:1px solid #bbf7d0;border-radius:10px;padding:12px 14px;"> 570 <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#16a34a" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" style="flex-shrink:0;margin-top:1px;"><polyline points="20 6 9 17 4 12"/></svg> 571 <div style="font-size:13px;color:#166534;line-height:1.5;"> 572 <strong><?php esc_html_e( 'Connected and active.', 'goultra-for-woocommerce' ); ?></strong> 573 <?php esc_html_e( 'Message delivery receipts appear as order notes, and your product catalog is available to GoUltra automatically. No secret or URL to copy anywhere.', 'goultra-for-woocommerce' ); ?> 574 </div> 575 </div> 576 577 <details style="font-size:12px;color:#64748b;"> 578 <summary style="cursor:pointer;font-weight:600;color:#475569;"><?php esc_html_e( 'Technical details (for support)', 'goultra-for-woocommerce' ); ?></summary> 579 <div style="margin-top:10px;display:flex;flex-direction:column;gap:8px;"> 580 <div style="display:flex;gap:8px;align-items:center;"> 581 <span style="min-width:110px;"><?php esc_html_e( 'Webhook secret', 'goultra-for-woocommerce' ); ?></span> 582 <code id="gu-wh-secret" style="flex:1;background:#fff;border:1px solid #e2e8f0;border-radius:6px;padding:6px 10px;font-size:11px;font-family:monospace;word-break:break-all;" dir="ltr"><?php echo esc_html( $webhook_secret ); ?></code> 583 <button type="button" data-copy="<?php esc_attr_e( 'Copy', 'goultra-for-woocommerce' ); ?>" data-copied="<?php esc_attr_e( '✓ Copied!', 'goultra-for-woocommerce' ); ?>" onclick="navigator.clipboard.writeText(document.getElementById('gu-wh-secret').textContent).then(()=>{var t=this;t.textContent=t.dataset.copied;setTimeout(()=>t.textContent=t.dataset.copy,1500)})" style="white-space:nowrap;padding:6px 12px;background:#0f172a;color:#fff;border:none;border-radius:6px;cursor:pointer;font-size:11px;font-weight:600;"><?php esc_html_e( 'Copy', 'goultra-for-woocommerce' ); ?></button> 584 </div> 585 <div dir="ltr" style="font-family:monospace;font-size:11px;word-break:break-all;"><?php echo esc_url( $base_url . '/webhook' ); ?></div> 586 <div dir="ltr" style="font-family:monospace;font-size:11px;word-break:break-all;"><?php echo esc_url( $base_url . '/sync' ); ?></div> 587 <div style="font-size:11px;color:#94a3b8;"><?php esc_html_e( 'The plugin shares the secret with GoUltra automatically on connect, so you never paste it anywhere.', 'goultra-for-woocommerce' ); ?></div> 588 </div> 589 </details> 510 590 </div> 511 591 </div> … … 526 606 <?php endif; ?> 527 607 528 <div class="gu-sf-sep"></div>529 530 <span style="font-size:11px;color:var(--gu-slate);font-weight:600;"><?php esc_html_e( 'Send a test message (optional)', 'goultra-for-woocommerce' ); ?></span>531 <input id="gu-test-phone" type="text" placeholder="<?php esc_attr_e( 'WhatsApp number', 'goultra-for-woocommerce' ); ?>" title="<?php esc_attr_e( 'Send a test WhatsApp message to this number to check your setup. Include the country code.', 'goultra-for-woocommerce' ); ?>" class="gu-sf-input" style="width:150px;" dir="ltr" />532 <input id="gu-test-order" type="number" placeholder="<?php esc_attr_e( 'Order #', 'goultra-for-woocommerce' ); ?>" title="<?php esc_attr_e( 'Optional: an order number to pull sample data from.', 'goultra-for-woocommerce' ); ?>" class="gu-sf-input" style="width:90px;" />533 <button type="button" id="gu-test-btn" onclick="guTestSend()" class="gu-btn gu-btn-o" style="padding:8px 14px;font-size:12px;" title="<?php esc_attr_e( 'Send a test WhatsApp message to verify your connection.', 'goultra-for-woocommerce' ); ?>">534 <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>535 <?php esc_html_e( 'Test', 'goultra-for-woocommerce' ); ?>536 </button>537 <span id="gu-test-result" style="font-size:11px;color:var(--gu-success);font-weight:600;"></span>538 608 </div> 539 609 <div class="gu-sf-right"> … … 613 683 }); 614 684 685 // ── Admin-number verification (WhatsApp OTP) ── 686 (function(){ 687 var $msg = $('#gu-av-msg'); 688 function say(t, cls){ $msg.text(t || '').removeClass('bad ok').addClass(cls || ''); } 689 function busy($b, on, txt){ $b.prop('disabled', on); if(txt) $b.text(txt); } 690 var T = { 691 send: '<?php echo esc_js( __( 'Send code', 'goultra-for-woocommerce' ) ); ?>', 692 sending: '<?php echo esc_js( __( 'Sending...', 'goultra-for-woocommerce' ) ); ?>', 693 verify: '<?php echo esc_js( __( 'Verify', 'goultra-for-woocommerce' ) ); ?>', 694 verifying: '<?php echo esc_js( __( 'Verifying...', 'goultra-for-woocommerce' ) ); ?>', 695 needPhone: '<?php echo esc_js( __( 'Enter your WhatsApp number first.', 'goultra-for-woocommerce' ) ); ?>', 696 needCode: '<?php echo esc_js( __( 'Enter the code you received.', 'goultra-for-woocommerce' ) ); ?>', 697 failSend: '<?php echo esc_js( __( 'Could not send the code.', 'goultra-for-woocommerce' ) ); ?>', 698 failCode: '<?php echo esc_js( __( 'Incorrect code.', 'goultra-for-woocommerce' ) ); ?>', 699 net: '<?php echo esc_js( __( 'Network error. Please try again.', 'goultra-for-woocommerce' ) ); ?>' 700 }; 701 $('#gu-av-again').on('click', function(){ 702 $('#gu-av-done').hide(); $('#gu-av-codewrap').hide(); $('#gu-av-start').show(); say(''); 703 $('#gu-av-phone').val('').focus(); 704 }); 705 function sendCode(){ 706 var phone = $.trim($('#gu-av-phone').val() || ''); 707 if(!phone){ say(T.needPhone, 'bad'); return; } 708 var $b = $('#gu-av-send'); busy($b, true, T.sending); 709 $.post(_ajax, {action:'goultra_admin_verify_send', nonce:_nonce, phone:phone}) 710 .done(function(r){ 711 busy($b, false, T.send); 712 if(r && r.success){ 713 $('#gu-av-start').hide(); $('#gu-av-codewrap').show(); $('#gu-av-code').val('').focus(); 714 say((r.data && r.data.message) || '', 'ok'); 715 } else { 716 say((r && r.data && r.data.message) ? r.data.message : T.failSend, 'bad'); 717 } 718 }) 719 .fail(function(){ busy($b, false, T.send); say(T.net, 'bad'); }); 720 } 721 $('#gu-av-send').on('click', sendCode); 722 $('#gu-av-resend').on('click', function(e){ e.preventDefault(); $('#gu-av-codewrap').hide(); $('#gu-av-start').show(); sendCode(); }); 723 $('#gu-av-verify').on('click', function(){ 724 var code = $.trim($('#gu-av-code').val() || ''); 725 if(code.length < 4){ say(T.needCode, 'bad'); return; } 726 var $b = $('#gu-av-verify'); busy($b, true, T.verifying); 727 $.post(_ajax, {action:'goultra_admin_verify_check', nonce:_nonce, code:code}) 728 .done(function(r){ 729 busy($b, false, T.verify); 730 if(r && r.success && r.data && r.data.verified){ 731 $('#gu-av-codewrap').hide(); 732 $('#gu-av-num').text(r.data.masked || ''); 733 $('#gu-av-done').show(); 734 say((r.data.message) || '', 'ok'); 735 } else { 736 say((r && r.data && r.data.message) ? r.data.message : T.failCode, 'bad'); 737 } 738 }) 739 .fail(function(){ busy($b, false, T.verify); say(T.net, 'bad'); }); 740 }); 741 })(); 742 615 743 $('#gu-settings-form').on('submit', function(e){ 616 744 e.preventDefault(); 617 745 var $b = $('#gu-save-btn').prop('disabled', true).text('<?php echo esc_js( __( 'Saving...', 'goultra-for-woocommerce' ) ); ?>'); 618 746 var data = $(this).serialize() + '&action=goultra_save_settings&nonce=' + _nonce; 619 ['enable_order_confirm','enable_shipping_update','enable_admin_notify','enable_abandoned_cart','enable_quiet_hours','ac_step1_enabled','ac_step2_enabled','ac_step3_enabled' ].forEach(function(n){747 ['enable_order_confirm','enable_shipping_update','enable_admin_notify','enable_abandoned_cart','enable_quiet_hours','ac_step1_enabled','ac_step2_enabled','ac_step3_enabled','chat_button_enabled','chat_button_product'].forEach(function(n){ 620 748 if(!$('[name="'+n+'"]').is(':checked')) data += '&' + n + '=no'; 621 749 }); -
goultra-for-woocommerce/trunk/templates/admin/subscribers-page.php
r3603449 r3617733 28 28 <div class="gu-hdr-links"> 29 29 <a href="<?php echo esc_url( admin_url( 'admin.php?page=goultra-settings' ) ); ?>"><?php esc_html_e( 'Settings', 'goultra-for-woocommerce' ); ?></a> 30 <a href="<?php echo esc_url( admin_url( 'admin.php?page=goultra-templates' ) ); ?>"><?php esc_html_e( 'Templates', 'goultra-for-woocommerce' ); ?></a> 31 <a href="<?php echo esc_url( admin_url( 'admin.php?page=goultra-campaigns' ) ); ?>"><?php esc_html_e( 'Campaigns', 'goultra-for-woocommerce' ); ?></a> 30 32 <a href="<?php echo esc_url( admin_url( 'admin.php?page=goultra-analytics' ) ); ?>"><?php esc_html_e( 'Analytics', 'goultra-for-woocommerce' ); ?></a> 33 <a href="<?php echo esc_url( admin_url( 'admin.php?page=goultra-logs' ) ); ?>"><?php esc_html_e( 'Logs', 'goultra-for-woocommerce' ); ?></a> 34 <a href="https://goultra.ai/docs/woocommerce" target="_blank"><?php esc_html_e( 'Docs', 'goultra-for-woocommerce' ); ?></a> 35 <a href="https://goultra.ai/support" target="_blank"><?php esc_html_e( 'Support', 'goultra-for-woocommerce' ); ?></a> 31 36 </div> 32 37 </div> … … 71 76 <option value="withdrawn"><?php esc_html_e( 'Unsubscribed', 'goultra-for-woocommerce' ); ?></option> 72 77 </select> 78 <button type="button" class="gu-btn gu-btn-p" id="gu-subs-add-btn" style="padding:8px 14px;font-size:12px;">+ <?php esc_html_e( 'Add my number', 'goultra-for-woocommerce' ); ?></button> 73 79 <a href="<?php echo esc_url( $gu_export ); ?>" class="gu-btn gu-btn-o" id="gu-subs-export" style="padding:8px 14px;font-size:12px;"><?php esc_html_e( 'Export CSV', 'goultra-for-woocommerce' ); ?></a> 80 </div> 81 82 <div id="gu-subs-add" class="gu-ml-add" style="display:none;"> 83 <div class="gu-ml-add-fields"> 84 <input type="text" id="gu-subs-add-name" placeholder="<?php esc_attr_e( 'Name', 'goultra-for-woocommerce' ); ?>" value="<?php echo esc_attr( wp_get_current_user()->display_name ); ?>" /> 85 <input type="tel" id="gu-subs-add-phone" dir="ltr" placeholder="<?php esc_attr_e( 'WhatsApp number', 'goultra-for-woocommerce' ); ?>" value="<?php echo esc_attr( get_option( GOULTRA_WC_OPTION_PREFIX . 'admin_phone', '' ) ); ?>" /> 86 <button type="button" class="gu-btn gu-btn-cy" id="gu-subs-add-save" style="padding:9px 18px;font-size:12px;"><?php esc_html_e( 'Add to the list', 'goultra-for-woocommerce' ); ?></button> 87 <span class="gu-ml-import-status" id="gu-subs-add-msg"></span> 88 </div> 89 <p class="gu-desc" style="margin:8px 0 0;"><?php esc_html_e( 'Adds the number to the marketing list as a subscribed contact. Useful for sending yourself a test campaign: on the Campaigns screen choose "Choose specific contacts" and select only your number.', 'goultra-for-woocommerce' ); ?></p> 74 90 </div> 75 91 -
goultra-for-woocommerce/trunk/templates/admin/template-presets.php
r3588005 r3617733 565 565 ), 566 566 ), 567 'marketing_offer' => array( 568 'en' => array( 569 array('key'=>"sale",'style'=>"Sale",'tpl'=>"Hi {{first_name}}! 570 571 Big news 🎉 For a limited time we are running special discounts on our most-loved products. Don't miss it - the sale ends soon.",'header_type'=>"image",'footer'=>"Reply STOP to unsubscribe",'button_text'=>"",'button_url'=>""), 572 array('key'=>"flash_sale",'style'=>"Flash sale",'tpl'=>"Hi {{first_name}}! 573 574 ⚡ Flash sale - today only! Special prices on selected items, for the next few hours. Grab yours before they're gone.",'header_type'=>"image",'footer'=>"Reply STOP to unsubscribe",'button_text'=>"",'button_url'=>""), 575 array('key'=>"holiday",'style'=>"Holiday",'tpl'=>"Hi {{first_name}}! 576 577 Happy holidays from all of us 🎁 To celebrate, we prepared a special offer just for our WhatsApp members. We would love to treat you.",'header_type'=>"image",'footer'=>"Reply STOP to unsubscribe",'button_text'=>"",'button_url'=>""), 578 array('key'=>"launch",'style'=>"New product",'tpl'=>"Hi {{first_name}}! 579 580 It is finally here ✨ Our newest product just landed, and you get first access as a WhatsApp member. Be among the first to try it.",'header_type'=>"image",'footer'=>"Reply STOP to unsubscribe",'button_text'=>"",'button_url'=>""), 581 array('key'=>"back_in_stock",'style'=>"Back in stock",'tpl'=>"Hi {{first_name}}! 582 583 Good news - it's back! 🔁 The item you love is in stock again. It tends to sell out fast, so grab it while you can.",'header_type'=>"image",'footer'=>"Reply STOP to unsubscribe",'button_text'=>"",'button_url'=>""), 584 ), 585 'he' => array( 586 array('key'=>"sale",'style'=>"מבצע",'tpl'=>"היי {{first_name}}! 587 588 חדשות מרעישות 🎉 לזמן מוגבל: הנחות מיוחדות על המוצרים האהובים ביותר. אל תפספסו - המבצע מסתיים בקרוב.",'header_type'=>"image",'footer'=>"להסרה השב הסר",'button_text'=>"",'button_url'=>""), 589 array('key'=>"flash_sale",'style'=>"מבצע בזק",'tpl'=>"היי {{first_name}}! 590 591 ⚡ מבצע בזק - היום בלבד! מחירים מיוחדים על פריטים נבחרים, לשעות הקרובות. תפסו לפני שייגמר.",'header_type'=>"image",'footer'=>"להסרה השב הסר",'button_text'=>"",'button_url'=>""), 592 array('key'=>"holiday",'style'=>"חג",'tpl'=>"היי {{first_name}}! 593 594 חג שמח מכולנו 🎁 לכבוד החג הכנו הטבה מיוחדת רק לחברי ה-WhatsApp שלנו. מחכים לפנק אתכם.",'header_type'=>"image",'footer'=>"להסרה השב הסר",'button_text'=>"",'button_url'=>""), 595 array('key'=>"launch",'style'=>"מוצר חדש",'tpl'=>"היי {{first_name}}! 596 597 זה הגיע ✨ המוצר החדש שלנו נחת, ואתם מקבלים גישה ראשונים כחברי WhatsApp. היו בין הראשונים לנסות.",'header_type'=>"image",'footer'=>"להסרה השב הסר",'button_text'=>"",'button_url'=>""), 598 array('key'=>"back_in_stock",'style'=>"חזר למלאי",'tpl'=>"היי {{first_name}}! 599 600 חדשות טובות - חזר למלאי! 🔁 הפריט שאתם אוהבים שוב זמין. הוא נוטה להיגמר מהר, אז תפסו כל עוד אפשר.",'header_type'=>"image",'footer'=>"להסרה השב הסר",'button_text'=>"",'button_url'=>""), 601 ), 602 'ar' => array( 603 array('key'=>"sale",'style'=>"تخفيضات",'tpl'=>"مرحباً {{first_name}}! 604 605 أخبار رائعة 🎉 لفترة محدودة: خصومات خاصة على أكثر منتجاتنا حباً. لا تفوّت الفرصة - العرض ينتهي قريباً.",'header_type'=>"image",'footer'=>"للإزالة أرسل قف",'button_text'=>"",'button_url'=>""), 606 array('key'=>"flash_sale",'style'=>"عرض سريع",'tpl'=>"مرحباً {{first_name}}! 607 608 ⚡ عرض سريع - اليوم فقط! أسعار خاصة على منتجات مختارة، للساعات القادمة. اقتنِها قبل نفادها.",'header_type'=>"image",'footer'=>"للإزالة أرسل قف",'button_text'=>"",'button_url'=>""), 609 array('key'=>"holiday",'style'=>"عيد",'tpl'=>"مرحباً {{first_name}}! 610 611 أعياد سعيدة من فريقنا 🎁 بمناسبة الأعياد أعددنا عرضاً خاصاً لأعضاء WhatsApp فقط. يسعدنا أن ندللكم.",'header_type'=>"image",'footer'=>"للإزالة أرسل قف",'button_text'=>"",'button_url'=>""), 612 array('key'=>"launch",'style'=>"منتج جديد",'tpl'=>"مرحباً {{first_name}}! 613 614 وصل أخيراً ✨ أحدث منتجاتنا وصل، وتحصل على أولوية الوصول كعضو WhatsApp. كن من الأوائل لتجربته.",'header_type'=>"image",'footer'=>"للإزالة أرسل قف",'button_text'=>"",'button_url'=>""), 615 array('key'=>"back_in_stock",'style'=>"عاد للتوفر",'tpl'=>"مرحباً {{first_name}}! 616 617 خبر جميل - عاد للتوفر! 🔁 المنتج الذي تحبه متوفر من جديد. عادةً ما ينفد بسرعة، فاقتنِه ما دام متاحاً.",'header_type'=>"image",'footer'=>"للإزالة أرسل قف",'button_text'=>"",'button_url'=>""), 618 ), 619 'es' => array( 620 array('key'=>"sale",'style'=>"Rebajas",'tpl'=>"¡Hola {{first_name}}! 621 622 Grandes noticias 🎉 Por tiempo limitado: descuentos especiales en nuestros productos favoritos. No te lo pierdas: la oferta termina pronto.",'header_type'=>"image",'footer'=>"Responda PARAR para darse de baja",'button_text'=>"",'button_url'=>""), 623 array('key'=>"flash_sale",'style'=>"Oferta flash",'tpl'=>"¡Hola {{first_name}}! 624 625 ⚡ Oferta flash, ¡solo hoy! Precios especiales en artículos seleccionados, durante las próximas horas. Consíguelo antes de que se agote.",'header_type'=>"image",'footer'=>"Responda PARAR para darse de baja",'button_text'=>"",'button_url'=>""), 626 array('key'=>"holiday",'style'=>"Fiestas",'tpl'=>"¡Hola {{first_name}}! 627 628 ¡Felices fiestas de todo el equipo! 🎁 Para celebrar, preparamos una oferta especial solo para nuestros miembros de WhatsApp.",'header_type'=>"image",'footer'=>"Responda PARAR para darse de baja",'button_text'=>"",'button_url'=>""), 629 array('key'=>"launch",'style'=>"Novedad",'tpl'=>"¡Hola {{first_name}}! 630 631 ¡Por fin llegó! ✨ Nuestro producto más nuevo acaba de aterrizar, y como miembro de WhatsApp tienes acceso primero. Sé de los primeros en probarlo.",'header_type'=>"image",'footer'=>"Responda PARAR para darse de baja",'button_text'=>"",'button_url'=>""), 632 array('key'=>"back_in_stock",'style'=>"De nuevo disponible",'tpl'=>"¡Hola {{first_name}}! 633 634 Buenas noticias: ¡ha vuelto! 🔁 El producto que te encanta está disponible de nuevo. Suele agotarse rápido, aprovéchalo.",'header_type'=>"image",'footer'=>"Responda PARAR para darse de baja",'button_text'=>"",'button_url'=>""), 635 ), 636 'fr' => array( 637 array('key'=>"sale",'style'=>"Soldes",'tpl'=>"Bonjour {{first_name}} ! 638 639 Grande nouvelle 🎉 Pour une durée limitée : des remises spéciales sur nos produits préférés. Ne ratez pas ça - l'offre se termine bientôt.",'header_type'=>"image",'footer'=>"Répondez STOP pour vous désinscrire",'button_text'=>"",'button_url'=>""), 640 array('key'=>"flash_sale",'style'=>"Vente flash",'tpl'=>"Bonjour {{first_name}} ! 641 642 ⚡ Vente flash, aujourd'hui seulement ! Prix spéciaux sur une sélection, pour les prochaines heures. Profitez-en avant la fin.",'header_type'=>"image",'footer'=>"Répondez STOP pour vous désinscrire",'button_text'=>"",'button_url'=>""), 643 array('key'=>"holiday",'style'=>"Fêtes",'tpl'=>"Bonjour {{first_name}} ! 644 645 Joyeuses fêtes de toute l'équipe 🎁 Pour célébrer, une offre spéciale attend nos membres WhatsApp. On aimerait vous gâter.",'header_type'=>"image",'footer'=>"Répondez STOP pour vous désinscrire",'button_text'=>"",'button_url'=>""), 646 array('key'=>"launch",'style'=>"Nouveauté",'tpl'=>"Bonjour {{first_name}} ! 647 648 Il est enfin là ✨ Notre tout nouveau produit vient d'arriver, et en tant que membre WhatsApp vous y accédez en premier.",'header_type'=>"image",'footer'=>"Répondez STOP pour vous désinscrire",'button_text'=>"",'button_url'=>""), 649 array('key'=>"back_in_stock",'style'=>"De retour",'tpl'=>"Bonjour {{first_name}} ! 650 651 Bonne nouvelle, il est de retour ! 🔁 Le produit que vous aimez est de nouveau disponible. Il part vite, profitez-en.",'header_type'=>"image",'footer'=>"Répondez STOP pour vous désinscrire",'button_text'=>"",'button_url'=>""), 652 ), 653 'de' => array( 654 array('key'=>"sale",'style'=>"Sale",'tpl'=>"Hallo {{first_name}}! 655 656 Große Neuigkeiten 🎉 Für kurze Zeit gibt es besondere Rabatte auf unsere beliebtesten Produkte. Verpassen Sie es nicht - die Aktion endet bald.",'header_type'=>"image",'footer'=>"Antworten Sie STOPP zum Abbestellen",'button_text'=>"",'button_url'=>""), 657 array('key'=>"flash_sale",'style'=>"Blitzangebot",'tpl'=>"Hallo {{first_name}}! 658 659 ⚡ Blitzangebot, nur heute! Sonderpreise auf ausgewählte Artikel, für die nächsten Stunden. Greifen Sie zu, bevor sie weg sind.",'header_type'=>"image",'footer'=>"Antworten Sie STOPP zum Abbestellen",'button_text'=>"",'button_url'=>""), 660 array('key'=>"holiday",'style'=>"Feiertage",'tpl'=>"Hallo {{first_name}}! 661 662 Frohe Feiertage von uns allen 🎁 Zur Feier haben wir ein besonderes Angebot nur für unsere WhatsApp-Mitglieder.",'header_type'=>"image",'footer'=>"Antworten Sie STOPP zum Abbestellen",'button_text'=>"",'button_url'=>""), 663 array('key'=>"launch",'style'=>"Neuheit",'tpl'=>"Hallo {{first_name}}! 664 665 Endlich da ✨ Unser neuestes Produkt ist gelandet, und als WhatsApp-Mitglied erhalten Sie zuerst Zugriff.",'header_type'=>"image",'footer'=>"Antworten Sie STOPP zum Abbestellen",'button_text'=>"",'button_url'=>""), 666 array('key'=>"back_in_stock",'style'=>"Wieder da",'tpl'=>"Hallo {{first_name}}! 667 668 Gute Nachricht - wieder da! 🔁 Ihr Lieblingsprodukt ist wieder verfügbar. Es ist schnell vergriffen, also greifen Sie zu.",'header_type'=>"image",'footer'=>"Antworten Sie STOPP zum Abbestellen",'button_text'=>"",'button_url'=>""), 669 ), 670 'it' => array( 671 array('key'=>"sale",'style'=>"Saldi",'tpl'=>"Ciao {{first_name}}! 672 673 Grande novità 🎉 Per un periodo limitato: sconti speciali sui nostri prodotti più amati. Non perderteli: l'offerta finisce presto.",'header_type'=>"image",'footer'=>"Rispondi STOP per disiscriverti",'button_text'=>"",'button_url'=>""), 674 array('key'=>"flash_sale",'style'=>"Offerta lampo",'tpl'=>"Ciao {{first_name}}! 675 676 ⚡ Offerta lampo, solo oggi! Prezzi speciali su articoli selezionati, per le prossime ore. Approfittane prima che finiscano.",'header_type'=>"image",'footer'=>"Rispondi STOP per disiscriverti",'button_text'=>"",'button_url'=>""), 677 array('key'=>"holiday",'style'=>"Feste",'tpl'=>"Ciao {{first_name}}! 678 679 Buone feste da tutti noi 🎁 Per festeggiare, un'offerta speciale solo per i nostri membri WhatsApp. Ci farebbe piacere viziarti.",'header_type'=>"image",'footer'=>"Rispondi STOP per disiscriverti",'button_text'=>"",'button_url'=>""), 680 array('key'=>"launch",'style'=>"Novità",'tpl'=>"Ciao {{first_name}}! 681 682 Finalmente è qui ✨ Il nostro prodotto più nuovo è appena arrivato, e come membro WhatsApp hai accesso prioritario.",'header_type'=>"image",'footer'=>"Rispondi STOP per disiscriverti",'button_text'=>"",'button_url'=>""), 683 array('key'=>"back_in_stock",'style'=>"Di nuovo disponibile",'tpl'=>"Ciao {{first_name}}! 684 685 Buone notizie, è tornato! 🔁 Il prodotto che ami è di nuovo disponibile. Va a ruba, quindi approfittane subito.",'header_type'=>"image",'footer'=>"Rispondi STOP per disiscriverti",'button_text'=>"",'button_url'=>""), 686 ), 687 'pt' => array( 688 array('key'=>"sale",'style'=>"Promoção",'tpl'=>"Olá {{first_name}}! 689 690 Grande novidade 🎉 Por tempo limitado: descontos especiais nos nossos produtos mais amados. Não perca - a promoção acaba em breve.",'header_type'=>"image",'footer'=>"Responda PARAR para cancelar",'button_text'=>"",'button_url'=>""), 691 array('key'=>"flash_sale",'style'=>"Oferta relâmpago",'tpl'=>"Olá {{first_name}}! 692 693 ⚡ Oferta relâmpago, só hoje! Preços especiais em itens selecionados, nas próximas horas. Garanta antes que acabe.",'header_type'=>"image",'footer'=>"Responda PARAR para cancelar",'button_text'=>"",'button_url'=>""), 694 array('key'=>"holiday",'style'=>"Festas",'tpl'=>"Olá {{first_name}}! 695 696 Boas festas de toda a equipe 🎁 Para comemorar, uma oferta especial só para os nossos membros do WhatsApp.",'header_type'=>"image",'footer'=>"Responda PARAR para cancelar",'button_text'=>"",'button_url'=>""), 697 array('key'=>"launch",'style'=>"Novidade",'tpl'=>"Olá {{first_name}}! 698 699 Finalmente chegou ✨ Nosso produto mais novo acabou de chegar, e como membro do WhatsApp você tem acesso primeiro.",'header_type'=>"image",'footer'=>"Responda PARAR para cancelar",'button_text'=>"",'button_url'=>""), 700 array('key'=>"back_in_stock",'style'=>"De volta ao estoque",'tpl'=>"Olá {{first_name}}! 701 702 Boa notícia, voltou! 🔁 O produto que você ama está disponível de novo. Costuma esgotar rápido, aproveite.",'header_type'=>"image",'footer'=>"Responda PARAR para cancelar",'button_text'=>"",'button_url'=>""), 703 ), 704 'ru' => array( 705 array('key'=>"sale",'style'=>"Распродажа",'tpl'=>"Здравствуйте, {{first_name}}! 706 707 Отличные новости 🎉 Ограниченное время: специальные скидки на наши самые любимые товары. Не упустите - акция скоро закончится.",'header_type'=>"image",'footer'=>"Ответьте СТОП для отписки",'button_text'=>"",'button_url'=>""), 708 array('key'=>"flash_sale",'style'=>"Быстрая акция",'tpl'=>"Здравствуйте, {{first_name}}! 709 710 ⚡ Быстрая акция - только сегодня! Специальные цены на избранные товары, на ближайшие часы. Успейте, пока не разобрали.",'header_type'=>"image",'footer'=>"Ответьте СТОП для отписки",'button_text'=>"",'button_url'=>""), 711 array('key'=>"holiday",'style'=>"Праздник",'tpl'=>"Здравствуйте, {{first_name}}! 712 713 С праздником от всей команды 🎁 В честь праздника - специальное предложение только для участников WhatsApp.",'header_type'=>"image",'footer'=>"Ответьте СТОП для отписки",'button_text'=>"",'button_url'=>""), 714 array('key'=>"launch",'style'=>"Новинка",'tpl'=>"Здравствуйте, {{first_name}}! 715 716 Наконец-то ✨ Наша новинка только что появилась, и как участник WhatsApp вы получаете доступ первыми.",'header_type'=>"image",'footer'=>"Ответьте СТОП для отписки",'button_text'=>"",'button_url'=>""), 717 array('key'=>"back_in_stock",'style'=>"Снова в наличии",'tpl'=>"Здравствуйте, {{first_name}}! 718 719 Хорошая новость - снова в наличии! 🔁 Любимый товар снова доступен. Разбирают быстро, успевайте.",'header_type'=>"image",'footer'=>"Ответьте СТОП для отписки",'button_text'=>"",'button_url'=>""), 720 ), 721 'tr' => array( 722 array('key'=>"sale",'style'=>"İndirim",'tpl'=>"Merhaba {{first_name}}! 723 724 Büyük haber 🎉 Sınırlı süreyle en sevilen ürünlerimizde özel indirimler. Kaçırmayın - kampanya yakında bitiyor.",'header_type'=>"image",'footer'=>"Aboneliği iptal etmek için DUR yazın",'button_text'=>"",'button_url'=>""), 725 array('key'=>"flash_sale",'style'=>"Şimşek indirim",'tpl'=>"Merhaba {{first_name}}! 726 727 ⚡ Şimşek indirim - sadece bugün! Seçili ürünlerde özel fiyatlar, önümüzdeki birkaç saat için. Bitmeden yakalayın.",'header_type'=>"image",'footer'=>"Aboneliği iptal etmek için DUR yazın",'button_text'=>"",'button_url'=>""), 728 array('key'=>"holiday",'style'=>"Bayram",'tpl'=>"Merhaba {{first_name}}! 729 730 Hepimizden mutlu bayramlar 🎁 Kutlamak için WhatsApp üyelerimize özel bir teklif hazırladık.",'header_type'=>"image",'footer'=>"Aboneliği iptal etmek için DUR yazın",'button_text'=>"",'button_url'=>""), 731 array('key'=>"launch",'style'=>"Yeni ürün",'tpl'=>"Merhaba {{first_name}}! 732 733 Sonunda geldi ✨ En yeni ürünümüz az önce çıktı ve WhatsApp üyesi olarak önce siz erişiyorsunuz.",'header_type'=>"image",'footer'=>"Aboneliği iptal etmek için DUR yazın",'button_text'=>"",'button_url'=>""), 734 array('key'=>"back_in_stock",'style'=>"Yeniden stokta",'tpl'=>"Merhaba {{first_name}}! 735 736 İyi haber - yeniden stokta! 🔁 Sevdiğiniz ürün yeniden mevcut. Hızlı tükeniyor, fırsatı kaçırmayın.",'header_type'=>"image",'footer'=>"Aboneliği iptal etmek için DUR yazın",'button_text'=>"",'button_url'=>""), 737 ), 738 'nl' => array( 739 array('key'=>"sale",'style'=>"Sale",'tpl'=>"Hoi {{first_name}}! 740 741 Groot nieuws 🎉 Voor korte tijd: speciale kortingen op onze populairste producten. Mis het niet - de actie loopt bijna af.",'header_type'=>"image",'footer'=>"Antwoord STOP om af te melden",'button_text'=>"",'button_url'=>""), 742 array('key'=>"flash_sale",'style'=>"Flash sale",'tpl'=>"Hoi {{first_name}}! 743 744 ⚡ Flash sale, alleen vandaag! Speciale prijzen op geselecteerde items, de komende uren. Wees er snel bij.",'header_type'=>"image",'footer'=>"Antwoord STOP om af te melden",'button_text'=>"",'button_url'=>""), 745 array('key'=>"holiday",'style'=>"Feestdagen",'tpl'=>"Hoi {{first_name}}! 746 747 Fijne feestdagen van ons allemaal 🎁 Om te vieren een speciale aanbieding alleen voor onze WhatsApp-leden.",'header_type'=>"image",'footer'=>"Antwoord STOP om af te melden",'button_text'=>"",'button_url'=>""), 748 array('key'=>"launch",'style'=>"Nieuw",'tpl'=>"Hoi {{first_name}}! 749 750 Eindelijk is het er ✨ Ons nieuwste product is net geland, en als WhatsApp-lid krijg jij als eerste toegang.",'header_type'=>"image",'footer'=>"Antwoord STOP om af te melden",'button_text'=>"",'button_url'=>""), 751 array('key'=>"back_in_stock",'style'=>"Weer op voorraad",'tpl'=>"Hoi {{first_name}}! 752 753 Goed nieuws - weer op voorraad! 🔁 Jouw favoriet is weer beschikbaar. Snel uitverkocht, dus wees er bij.",'header_type'=>"image",'footer'=>"Antwoord STOP om af te melden",'button_text'=>"",'button_url'=>""), 754 ), 755 'pl' => array( 756 array('key'=>"sale",'style'=>"Wyprzedaż",'tpl'=>"Cześć {{first_name}}! 757 758 Świetne wieści 🎉 Przez ograniczony czas: specjalne rabaty na nasze najbardziej lubiane produkty. Nie przegap - promocja wkrótce się kończy.",'header_type'=>"image",'footer'=>"Wyślij STOP aby zrezygnować",'button_text'=>"",'button_url'=>""), 759 array('key'=>"flash_sale",'style'=>"Błyskawiczna",'tpl'=>"Cześć {{first_name}}! 760 761 ⚡ Wyprzedaż błyskawiczna - tylko dziś! Specjalne ceny na wybrane produkty, przez najbliższe godziny. Zdążysz przed wyprzedaniem.",'header_type'=>"image",'footer'=>"Wyślij STOP aby zrezygnować",'button_text'=>"",'button_url'=>""), 762 array('key'=>"holiday",'style'=>"Święta",'tpl'=>"Cześć {{first_name}}! 763 764 Wesołych świąt od całego zespołu 🎁 Z tej okazji specjalna oferta tylko dla członków WhatsApp.",'header_type'=>"image",'footer'=>"Wyślij STOP aby zrezygnować",'button_text'=>"",'button_url'=>""), 765 array('key'=>"launch",'style'=>"Nowość",'tpl'=>"Cześć {{first_name}}! 766 767 Wreszcie jest ✨ Nasz najnowszy produkt właśnie się pojawił, a jako członek WhatsApp masz dostęp jako pierwszy.",'header_type'=>"image",'footer'=>"Wyślij STOP aby zrezygnować",'button_text'=>"",'button_url'=>""), 768 array('key'=>"back_in_stock",'style'=>"Znów dostępne",'tpl'=>"Cześć {{first_name}}! 769 770 Dobra wiadomość - znów dostępne! 🔁 Twój ulubiony produkt jest znów w sprzedaży. Szybko się rozchodzi, więc nie zwlekaj.",'header_type'=>"image",'footer'=>"Wyślij STOP aby zrezygnować",'button_text'=>"",'button_url'=>""), 771 ), 772 'uk' => array( 773 array('key'=>"sale",'style'=>"Розпродаж",'tpl'=>"Вітаємо, {{first_name}}! 774 775 Чудові новини 🎉 Обмежений час: спеціальні знижки на наші найулюбленіші товари. Не пропустіть - акція скоро завершується.",'header_type'=>"image",'footer'=>"Відповідайте СТОП для відписки",'button_text'=>"",'button_url'=>""), 776 array('key'=>"flash_sale",'style'=>"Швидка акція",'tpl'=>"Вітаємо, {{first_name}}! 777 778 ⚡ Швидка акція - лише сьогодні! Спеціальні ціни на вибрані товари, на найближчі години. Встигніть, поки не розібрали.",'header_type'=>"image",'footer'=>"Відповідайте СТОП для відписки",'button_text'=>"",'button_url'=>""), 779 array('key'=>"holiday",'style'=>"Свято",'tpl'=>"Вітаємо, {{first_name}}! 780 781 Зі святом від усієї команди 🎁 З нагоди свята - спеціальна пропозиція лише для учасників WhatsApp.",'header_type'=>"image",'footer'=>"Відповідайте СТОП для відписки",'button_text'=>"",'button_url'=>""), 782 array('key'=>"launch",'style'=>"Новинка",'tpl'=>"Вітаємо, {{first_name}}! 783 784 Нарешті це тут ✨ Наша новинка щойно з'явилася, і як учасник WhatsApp ви отримуєте доступ першими.",'header_type'=>"image",'footer'=>"Відповідайте СТОП для відписки",'button_text'=>"",'button_url'=>""), 785 array('key'=>"back_in_stock",'style'=>"Знову в наявності",'tpl'=>"Вітаємо, {{first_name}}! 786 787 Гарна новина - знову в наявності! 🔁 Улюблений товар знову доступний. Розкуповують швидко, поспішайте.",'header_type'=>"image",'footer'=>"Відповідайте СТОП для відписки",'button_text'=>"",'button_url'=>""), 788 ), 789 'id' => array( 790 array('key'=>"sale",'style'=>"Diskon",'tpl'=>"Hai {{first_name}}! 791 792 Kabar besar 🎉 Untuk waktu terbatas: diskon spesial untuk produk favorit kami. Jangan lewatkan - promo segera berakhir.",'header_type'=>"image",'footer'=>"Balas STOP untuk berhenti berlangganan",'button_text'=>"",'button_url'=>""), 793 array('key'=>"flash_sale",'style'=>"Kilat",'tpl'=>"Hai {{first_name}}! 794 795 ⚡ Diskon kilat - hari ini saja! Harga spesial untuk item pilihan, beberapa jam ke depan. Ambil sebelum habis.",'header_type'=>"image",'footer'=>"Balas STOP untuk berhenti berlangganan",'button_text'=>"",'button_url'=>""), 796 array('key'=>"holiday",'style'=>"Hari raya",'tpl'=>"Hai {{first_name}}! 797 798 Selamat hari raya dari kami semua 🎁 Untuk merayakannya, penawaran khusus hanya untuk member WhatsApp kami.",'header_type'=>"image",'footer'=>"Balas STOP untuk berhenti berlangganan",'button_text'=>"",'button_url'=>""), 799 array('key'=>"launch",'style'=>"Produk baru",'tpl'=>"Hai {{first_name}}! 800 801 Akhirnya hadir ✨ Produk terbaru kami baru saja tiba, dan sebagai member WhatsApp kamu dapat akses lebih dulu.",'header_type'=>"image",'footer'=>"Balas STOP untuk berhenti berlangganan",'button_text'=>"",'button_url'=>""), 802 array('key'=>"back_in_stock",'style'=>"Tersedia lagi",'tpl'=>"Hai {{first_name}}! 803 804 Kabar baik - tersedia lagi! 🔁 Produk favoritmu tersedia kembali. Biasanya cepat habis, ambil sekarang.",'header_type'=>"image",'footer'=>"Balas STOP untuk berhenti berlangganan",'button_text'=>"",'button_url'=>""), 805 ), 806 'ms' => array( 807 array('key'=>"sale",'style'=>"Jualan",'tpl'=>"Hai {{first_name}}! 808 809 Berita besar 🎉 Untuk masa terhad: diskaun istimewa untuk produk paling digemari kami. Jangan lepaskan - jualan tamat tidak lama lagi.",'header_type'=>"image",'footer'=>"Balas STOP untuk berhenti melanggan",'button_text'=>"",'button_url'=>""), 810 array('key'=>"flash_sale",'style'=>"Kilat",'tpl'=>"Hai {{first_name}}! 811 812 ⚡ Jualan kilat - hari ini sahaja! Harga istimewa untuk item terpilih, beberapa jam akan datang. Dapatkan sebelum habis.",'header_type'=>"image",'footer'=>"Balas STOP untuk berhenti melanggan",'button_text'=>"",'button_url'=>""), 813 array('key'=>"holiday",'style'=>"Perayaan",'tpl'=>"Hai {{first_name}}! 814 815 Selamat menyambut perayaan daripada kami semua 🎁 Untuk meraikannya, tawaran istimewa khas untuk ahli WhatsApp kami.",'header_type'=>"image",'footer'=>"Balas STOP untuk berhenti melanggan",'button_text'=>"",'button_url'=>""), 816 array('key'=>"launch",'style'=>"Produk baharu",'tpl'=>"Hai {{first_name}}! 817 818 Akhirnya tiba ✨ Produk terbaharu kami baru sahaja tiba, dan sebagai ahli WhatsApp anda dapat akses dahulu.",'header_type'=>"image",'footer'=>"Balas STOP untuk berhenti melanggan",'button_text'=>"",'button_url'=>""), 819 array('key'=>"back_in_stock",'style'=>"Kembali ada stok",'tpl'=>"Hai {{first_name}}! 820 821 Berita baik - kembali ada stok! 🔁 Produk kegemaran anda tersedia semula. Cepat habis, jadi dapatkannya sekarang.",'header_type'=>"image",'footer'=>"Balas STOP untuk berhenti melanggan",'button_text'=>"",'button_url'=>""), 822 ), 823 'vi' => array( 824 array('key'=>"sale",'style'=>"Khuyến mãi",'tpl'=>"Chào {{first_name}}! 825 826 Tin lớn đây 🎉 Trong thời gian giới hạn: giảm giá đặc biệt cho những sản phẩm được yêu thích nhất. Đừng bỏ lỡ - chương trình sắp kết thúc.",'header_type'=>"image",'footer'=>"Trả lời STOP để hủy đăng ký",'button_text'=>"",'button_url'=>""), 827 array('key'=>"flash_sale",'style'=>"Giờ vàng",'tpl'=>"Chào {{first_name}}! 828 829 ⚡ Giờ vàng - chỉ hôm nay! Giá đặc biệt cho các sản phẩm chọn lọc, trong vài giờ tới. Nhanh tay trước khi hết.",'header_type'=>"image",'footer'=>"Trả lời STOP để hủy đăng ký",'button_text'=>"",'button_url'=>""), 830 array('key'=>"holiday",'style'=>"Lễ hội",'tpl'=>"Chào {{first_name}}! 831 832 Chúc mừng ngày lễ từ tất cả chúng tôi 🎁 Để chào mừng, ưu đãi đặc biệt chỉ dành cho thành viên WhatsApp.",'header_type'=>"image",'footer'=>"Trả lời STOP để hủy đăng ký",'button_text'=>"",'button_url'=>""), 833 array('key'=>"launch",'style'=>"Sản phẩm mới",'tpl'=>"Chào {{first_name}}! 834 835 Cuối cùng đã có mặt ✨ Sản phẩm mới nhất vừa ra mắt, và là thành viên WhatsApp bạn được trải nghiệm trước.",'header_type'=>"image",'footer'=>"Trả lời STOP để hủy đăng ký",'button_text'=>"",'button_url'=>""), 836 array('key'=>"back_in_stock",'style'=>"Có hàng lại",'tpl'=>"Chào {{first_name}}! 837 838 Tin vui - có hàng trở lại! 🔁 Sản phẩm bạn yêu thích đã có lại. Thường hết rất nhanh, hãy nhanh tay.",'header_type'=>"image",'footer'=>"Trả lời STOP để hủy đăng ký",'button_text'=>"",'button_url'=>""), 839 ), 840 'th' => array( 841 array('key'=>"sale",'style'=>"ลดราคา",'tpl'=>"สวัสดี {{first_name}}! 842 843 ข่าวใหญ่ 🎉 ช่วงเวลาจำกัด: ส่วนลดพิเศษสำหรับสินค้ายอดนิยมของเรา อย่าพลาด - โปรโมชั่นใกล้จะหมดแล้ว",'header_type'=>"image",'footer'=>"ตอบ STOP เพื่อยกเลิกการสมัคร",'button_text'=>"",'button_url'=>""), 844 array('key'=>"flash_sale",'style'=>"แฟลชเซล",'tpl'=>"สวัสดี {{first_name}}! 845 846 ⚡ แฟลชเซล - วันนี้เท่านั้น! ราคาพิเศษสำหรับสินค้าที่เลือก ในไม่กี่ชั่วโมงข้างหน้า รีบคว้าก่อนหมด",'header_type'=>"image",'footer'=>"ตอบ STOP เพื่อยกเลิกการสมัคร",'button_text'=>"",'button_url'=>""), 847 array('key'=>"holiday",'style'=>"เทศกาล",'tpl'=>"สวัสดี {{first_name}}! 848 849 สุขสันต์เทศกาลจากพวกเราทุกคน 🎁 เพื่อเฉลิมฉลอง ข้อเสนอพิเศษเฉพาะสมาชิก WhatsApp ของเรา",'header_type'=>"image",'footer'=>"ตอบ STOP เพื่อยกเลิกการสมัคร",'button_text'=>"",'button_url'=>""), 850 array('key'=>"launch",'style'=>"สินค้าใหม่",'tpl'=>"สวัสดี {{first_name}}! 851 852 มาแล้ว ✨ สินค้าใหม่ล่าสุดของเราเพิ่งมาถึง และในฐานะสมาชิก WhatsApp คุณได้สิทธิ์ก่อนใคร",'header_type'=>"image",'footer'=>"ตอบ STOP เพื่อยกเลิกการสมัคร",'button_text'=>"",'button_url'=>""), 853 array('key'=>"back_in_stock",'style'=>"กลับมามีสต็อก",'tpl'=>"สวัสดี {{first_name}}! 854 855 ข่าวดี - กลับมามีสต็อกแล้ว! 🔁 สินค้าที่คุณชอบมีอีกครั้ง มักจะหมดเร็ว รีบเลยตอนนี้",'header_type'=>"image",'footer'=>"ตอบ STOP เพื่อยกเลิกการสมัคร",'button_text'=>"",'button_url'=>""), 856 ), 857 'ja' => array( 858 array('key'=>"sale",'style'=>"セール",'tpl'=>"{{first_name}}さん、こんにちは! 859 860 ビッグニュース🎉 期間限定で、人気商品を特別価格でご提供中です。お見逃しなく - まもなく終了します。",'header_type'=>"image",'footer'=>"配信停止には STOP と返信してください",'button_text'=>"",'button_url'=>""), 861 array('key'=>"flash_sale",'style'=>"タイムセール",'tpl'=>"{{first_name}}さん、こんにちは! 862 863 ⚡ タイムセール、本日限り!対象商品が特別価格、これから数時間だけ。売り切れる前にどうぞ。",'header_type'=>"image",'footer'=>"配信停止には STOP と返信してください",'button_text'=>"",'button_url'=>""), 864 array('key'=>"holiday",'style'=>"祝日",'tpl'=>"{{first_name}}さん、こんにちは! 865 866 みんなから素敵な祝日を🎁 お祝いに、WhatsApp会員さま限定の特別オファーをご用意しました。",'header_type'=>"image",'footer'=>"配信停止には STOP と返信してください",'button_text'=>"",'button_url'=>""), 867 array('key'=>"launch",'style'=>"新商品",'tpl'=>"{{first_name}}さん、こんにちは! 868 869 ついに登場✨ 最新商品が入荷し、WhatsApp会員さまが先行してご利用いただけます。いち早くお試しください。",'header_type'=>"image",'footer'=>"配信停止には STOP と返信してください",'button_text'=>"",'button_url'=>""), 870 array('key'=>"back_in_stock",'style'=>"再入荷",'tpl'=>"{{first_name}}さん、こんにちは! 871 872 朗報 - 再入荷しました!🔁 お気に入りの商品が再び入荷。すぐ売り切れるので、お早めに。",'header_type'=>"image",'footer'=>"配信停止には STOP と返信してください",'button_text'=>"",'button_url'=>""), 873 ), 874 'ko' => array( 875 array('key'=>"sale",'style'=>"세일",'tpl'=>"{{first_name}}님, 안녕하세요! 876 877 빅뉴스 🎉 한정 기간 동안 가장 사랑받는 제품을 특별 할인해 드립니다. 놓치지 마세요 - 곧 종료됩니다.",'header_type'=>"image",'footer'=>"구독 취소하려면 STOP 회신",'button_text'=>"",'button_url'=>""), 878 array('key'=>"flash_sale",'style'=>"반짝세일",'tpl'=>"{{first_name}}님, 안녕하세요! 879 880 ⚡ 반짝세일, 오늘만! 선택 상품 특별가, 앞으로 몇 시간 동안만. 품절 전에 잡으세요.",'header_type'=>"image",'footer'=>"구독 취소하려면 STOP 회신",'button_text'=>"",'button_url'=>""), 881 array('key'=>"holiday",'style'=>"연휴",'tpl'=>"{{first_name}}님, 안녕하세요! 882 883 저희 모두가 전하는 행복한 연휴 🎁 기념으로 WhatsApp 회원님만을 위한 특별 혜택을 준비했어요.",'header_type'=>"image",'footer'=>"구독 취소하려면 STOP 회신",'button_text'=>"",'button_url'=>""), 884 array('key'=>"launch",'style'=>"신제품",'tpl'=>"{{first_name}}님, 안녕하세요! 885 886 드디어 출시 ✨ 최신 제품이 방금 입고되었고, WhatsApp 회원님이 가장 먼저 만나보실 수 있어요.",'header_type'=>"image",'footer'=>"구독 취소하려면 STOP 회신",'button_text'=>"",'button_url'=>""), 887 array('key'=>"back_in_stock",'style'=>"재입고",'tpl'=>"{{first_name}}님, 안녕하세요! 888 889 희소식 - 재입고! 🔁 좋아하시는 제품이 다시 입고됐어요. 금방 품절되니 서두르세요.",'header_type'=>"image",'footer'=>"구독 취소하려면 STOP 회신",'button_text'=>"",'button_url'=>""), 890 ), 891 'zh' => array( 892 array('key'=>"sale",'style'=>"促销",'tpl'=>"{{first_name}},你好! 893 894 重磅消息 🎉 限时优惠:我们最受欢迎的产品特别折扣。不要错过 - 活动即将结束。",'header_type'=>"image",'footer'=>"回复 STOP 取消订阅",'button_text'=>"",'button_url'=>""), 895 array('key'=>"flash_sale",'style'=>"限时秒杀",'tpl'=>"{{first_name}},你好! 896 897 ⚡ 限时秒杀,仅限今日!精选商品特价,只在接下来几小时。抢完为止。",'header_type'=>"image",'footer'=>"回复 STOP 取消订阅",'button_text'=>"",'button_url'=>""), 898 array('key'=>"holiday",'style'=>"节日",'tpl'=>"{{first_name}},你好! 899 900 全体祝您节日快乐 🎁 为庆祝节日,我们为 WhatsApp 会员准备了专属优惠。",'header_type'=>"image",'footer'=>"回复 STOP 取消订阅",'button_text'=>"",'button_url'=>""), 901 array('key'=>"launch",'style'=>"新品",'tpl'=>"{{first_name}},你好! 902 903 终于来了 ✨ 我们的最新产品刚刚上架,作为 WhatsApp 会员您可抢先体验。",'header_type'=>"image",'footer'=>"回复 STOP 取消订阅",'button_text'=>"",'button_url'=>""), 904 array('key'=>"back_in_stock",'style'=>"补货上架",'tpl'=>"{{first_name}},你好! 905 906 好消息 - 补货啦!🔁 您喜欢的商品又有货了。经常很快售罄,抓紧入手。",'header_type'=>"image",'footer'=>"回复 STOP 取消订阅",'button_text'=>"",'button_url'=>""), 907 ), 908 'hi' => array( 909 array('key'=>"sale",'style'=>"सेल",'tpl'=>"नमस्ते {{first_name}}! 910 911 बड़ी खबर 🎉 सीमित समय के लिए: हमारे सबसे पसंदीदा उत्पादों पर खास छूट। चूकिए मत - ऑफ़र जल्द ख़त्म हो रहा है।",'header_type'=>"image",'footer'=>"सदस्यता रद्द करने के लिए STOP जवाब दें",'button_text'=>"",'button_url'=>""), 912 array('key'=>"flash_sale",'style'=>"फ्लैश सेल",'tpl'=>"नमस्ते {{first_name}}! 913 914 ⚡ फ्लैश सेल - सिर्फ़ आज! चुनिंदा उत्पादों पर खास कीमतें, अगले कुछ घंटों के लिए। ख़त्म होने से पहले ले लें।",'header_type'=>"image",'footer'=>"सदस्यता रद्द करने के लिए STOP जवाब दें",'button_text'=>"",'button_url'=>""), 915 array('key'=>"holiday",'style'=>"त्योहार",'tpl'=>"नमस्ते {{first_name}}! 916 917 हम सबकी ओर से त्योहार की शुभकामनाएँ 🎁 जश्न के लिए सिर्फ़ अपने WhatsApp सदस्यों के लिए खास ऑफ़र।",'header_type'=>"image",'footer'=>"सदस्यता रद्द करने के लिए STOP जवाब दें",'button_text'=>"",'button_url'=>""), 918 array('key'=>"launch",'style'=>"नया उत्पाद",'tpl'=>"नमस्ते {{first_name}}! 919 920 आख़िरकार आ गया ✨ हमारा नया उत्पाद अभी आया है, और WhatsApp सदस्य के रूप में आपको पहले पहुँच मिलती है।",'header_type'=>"image",'footer'=>"सदस्यता रद्द करने के लिए STOP जवाब दें",'button_text'=>"",'button_url'=>""), 921 array('key'=>"back_in_stock",'style'=>"फिर उपलब्ध",'tpl'=>"नमस्ते {{first_name}}! 922 923 अच्छी खबर - फिर उपलब्ध! 🔁 आपका पसंदीदा उत्पाद फिर से उपलब्ध है। यह जल्दी बिक जाता है, तो जल्दी करें।",'header_type'=>"image",'footer'=>"सदस्यता रद्द करने के लिए STOP जवाब दें",'button_text'=>"",'button_url'=>""), 924 ), 925 ), 567 926 ); -
goultra-for-woocommerce/trunk/templates/admin/templates-page.php
r3588005 r3617733 22 22 'abandoned_cart_2' => __( 'Abandoned Cart - Step 2 (Urgency)', 'goultra-for-woocommerce' ), 23 23 'abandoned_cart_3' => __( 'Abandoned Cart - Step 3 (Discount)', 'goultra-for-woocommerce' ), 24 'marketing_offer' => __( 'Marketing Offer', 'goultra-for-woocommerce' ), 24 25 'custom' => __( 'Custom Template', 'goultra-for-woocommerce' ), 25 26 ); … … 32 33 'abandoned_cart_2' => array( 'icon' => 'cart2', 'bg' => '#fee2e2', 'desc' => __( 'Step 2: Create urgency (4 hours)', 'goultra-for-woocommerce' ) ), 33 34 'abandoned_cart_3' => array( 'icon' => 'cart3', 'bg' => '#ccfbf1', 'desc' => __( 'Step 3: Offer discount (24 hours)', 'goultra-for-woocommerce' ) ), 35 'marketing_offer' => array( 'icon' => 'offer', 'bg' => '#fce7f3', 'desc' => __( 'Promotions and offers for opted-in subscribers', 'goultra-for-woocommerce' ) ), 34 36 'custom' => array( 'icon' => 'custom', 'bg' => '#f1f5f9', 'desc' => __( 'Write your own template with any variables', 'goultra-for-woocommerce' ) ), 35 37 ); … … 62 64 $saved_by_key[ $tk ][ $t['language'] ] = $st; 63 65 64 // Collect ALL template rows per type+lang for per-variant matching in JS 66 // Collect ALL template rows per type+lang for per-variant matching in JS. 67 // Marketing rows also carry their body: older marketing template names have no 68 // style segment, so the JS identifies which style card they belong to by 69 // comparing the stored body against the preset text (variables stripped). 65 70 if ( ! isset( $all_tpls ) ) $all_tpls = array(); 66 71 $all_tpls[ $tk ][ $t['language'] ][] = array( … … 68 73 'style_key' => $t['style_key'] ?? '', 69 74 'template_name' => $t['template_name'] ?? '', 75 'body_text' => ( 'marketing_offer' === $tk ) ? (string) ( $t['body_text'] ?? '' ) : '', 70 76 ); 71 77 … … 80 86 } 81 87 88 // The custom-templates list = everything built in the custom builder, whatever its 89 // category: custom_* keys (UTILITY customs) AND marketing_offer rows that were written 90 // in the builder (style_key 'custom', or legacy style-less rows whose body does NOT 91 // match any preset - preset-built rows belong to their style cards, not this list). 92 // Without the marketing part, a MARKETING-category custom template vanished after 93 // submission (not in this list, not on a preset card) and looked like it never sent. 94 $gu_norm_body = function ( $s ) { 95 $s = preg_replace( '/\{\{[^}]*\}\}/', '', (string) $s ); 96 $s = preg_replace( '/[\x{200e}\x{200f}\x{2066}-\x{2069}\x{202a}-\x{202e}]/u', '', $s ); 97 return preg_replace( '/\s+/u', '', $s ); 98 }; 99 $gu_preset_bodies = array(); // lang => [ normalized preset body, ... ] 100 foreach ( ( $localized['marketing_offer'] ?? array() ) as $gu_pl => $gu_pvars ) { 101 foreach ( (array) $gu_pvars as $gu_pv ) { 102 $gu_preset_bodies[ $gu_pl ][] = $gu_norm_body( $gu_pv['tpl'] ?? '' ); 103 } 104 } 82 105 $custom_templates = array(); 83 106 foreach ( $templates as $t ) { 84 107 if ( strpos( $t['template_key'], 'custom' ) === 0 ) { 108 $custom_templates[] = $t; 109 continue; 110 } 111 if ( 'marketing_offer' === $t['template_key'] ) { 112 $sk = (string) ( $t['style_key'] ?? '' ); 113 if ( '' !== $sk && 'custom' !== $sk ) { 114 continue; // built from a preset style card 115 } 116 if ( 'custom' === $sk ) { 117 $custom_templates[] = $t; // definitively builder-made (new naming) 118 continue; 119 } 120 // Legacy style-less row: only its text can tell a preset build from a 121 // builder one - exclude it when the body equals a preset for its language. 122 $nb = $gu_norm_body( $t['body_text'] ?? '' ); 123 $lang = (string) ( $t['language'] ?? '' ); 124 $pool = $gu_preset_bodies[ $lang ] ?? ( $gu_preset_bodies[ substr( $lang, 0, 2 ) ] ?? array() ); 125 if ( '' !== $nb && in_array( $nb, $pool, true ) ) { 126 continue; // legacy preset-built row (identified by its text) 127 } 85 128 $custom_templates[] = $t; 86 129 } … … 127 170 'cart3' => '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#0d9488" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></svg>', 128 171 'custom' => '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#475569" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>', 172 'offer' => '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#db2777" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M20.59 13.41 13.42 20.59a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"/><line x1="7" y1="7" x2="7.01" y2="7"/></svg>', 129 173 ); 130 174 ?> … … 162 206 ?><span class="gu-tc-badge" style="background:#f1f5f9;color:#9ca3af;"><?php esc_html_e( 'Not set up', 'goultra-for-woocommerce' ); ?></span><?php 163 207 endif; ?> 164 <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#d1d5db" stroke-width="2"><polyline points="9 18 15 12 9 6"/></svg> 208 <?php // Direction-aware "forward" chevron: points LEFT in RTL (he/ar). ?> 209 <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#d1d5db" stroke-width="2"<?php echo $gu_rtl ? ' style="transform:scaleX(-1)"' : ''; ?>><polyline points="9 18 15 12 9 6"/></svg> 165 210 </div> 166 211 <?php endforeach; ?> … … 186 231 </div> 187 232 </div> 233 <input type="text" id="gu-custom-search" class="gu-search" style="margin-bottom:14px;" placeholder="<?php esc_attr_e( 'Search your templates by name, text or language...', 'goultra-for-woocommerce' ); ?>" autocomplete="off" /> 188 234 <div id="gu-custom-list"></div> 235 </div> 236 237 <!-- Custom template preview modal --> 238 <div id="gu-tpl-modal" style="display:none;position:fixed;inset:0;z-index:100000;background:rgba(15,23,42,0.55);align-items:center;justify-content:center;padding:20px;"> 239 <div id="gu-tpl-modal-card" style="background:#f3f4f6;border-radius:18px;max-width:420px;width:100%;max-height:90vh;overflow:auto;padding:18px;position:relative;"> 240 <button type="button" id="gu-tpl-modal-x" aria-label="<?php esc_attr_e( 'Close', 'goultra-for-woocommerce' ); ?>" style="position:absolute;top:12px;inset-inline-end:12px;width:30px;height:30px;border:none;border-radius:8px;background:#fff;color:#374151;font-size:18px;cursor:pointer;line-height:1;">×</button> 241 <div id="gu-tpl-modal-body"></div> 242 </div> 189 243 </div> 190 244 … … 212 266 <div id="gu-status-banner" style="display:none;margin-bottom:16px;padding:14px 20px;border-radius:12px;font-size:14px;"></div> 213 267 <div id="gu-ph" class="gu-phones-grid"></div> 214 <!-- Custom template editor --> 215 <div id="gu-custom-editor" style="display:none;"> 216 <div style="display:flex;gap:24px;flex-wrap:wrap;"> 217 <!-- LEFT: Editor --> 218 <div style="flex:1;min-width:340px;max-width:520px;"> 219 <div style="margin-bottom:16px;"> 220 <label style="font-size:13px;font-weight:600;color:#374151;display:block;margin-bottom:6px;"><?php esc_html_e( 'Available Variables', 'goultra-for-woocommerce' ); ?> <span style="font-weight:400;color:#9ca3af;"><?php esc_html_e( '(click to insert)', 'goultra-for-woocommerce' ); ?></span></label> 221 <div id="gu-var-chips" style="display:flex;flex-wrap:wrap;gap:6px;"></div> 222 </div> 223 <div id="gu-adj-warn" style="display:none;margin-bottom:12px;padding:10px 14px;border-radius:8px;background:#fef3c7;border:1px solid #fde68a;color:#92400e;font-size:13px;"> 224 ⚠️ <?php echo wp_kses( __( '<strong>Warning:</strong> Two variables next to each other may cause Meta to reject the template. Add text between them.', 'goultra-for-woocommerce' ), array( 'strong' => array() ) ); ?> 225 </div> 226 <div style="display:flex;gap:12px;margin-bottom:14px;"> 227 <div style="flex:1;"> 228 <label style="font-size:13px;font-weight:600;color:#374151;display:block;margin-bottom:6px;"><?php esc_html_e( 'Category', 'goultra-for-woocommerce' ); ?></label> 229 <select id="gu-custom-category" style="width:100%;padding:10px 32px 10px 14px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:14px;-webkit-appearance:none;appearance:none;background:white url('data:image/svg+xml;utf8,<svg xmlns=%22http://www.w3.org/2000/svg%22 width=%2212%22 height=%2212%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22%236b7280%22 stroke-width=%222%22><polyline points=%226 9 12 15 18 9%22/></svg>') no-repeat right 12px center;"> 230 <option value="UTILITY"><?php esc_html_e( 'Utility (order updates, shipping)', 'goultra-for-woocommerce' ); ?></option> 231 <option value="MARKETING"><?php esc_html_e( 'Marketing (promotions, cart recovery)', 'goultra-for-woocommerce' ); ?></option> 268 <!-- Custom / marketing template editor (rich builder) --> 269 <?php 270 $gu_wa_name = get_option( GOULTRA_WC_OPTION_PREFIX . 'whatsapp_display_name', '' ); 271 if ( '' === $gu_wa_name ) { $gu_wa_name = get_bloginfo( 'name' ); } 272 $gu_wa_avatar = GoUltra_Settings::wa_avatar_url(); 273 $gu_wa_fallback = (string) get_site_icon_url( 96 ); 274 $gu_wa_initial = function_exists( 'mb_substr' ) ? mb_substr( $gu_wa_name, 0, 1 ) : substr( $gu_wa_name, 0, 1 ); 275 ?> 276 <div id="gu-custom-editor" class="gu-cb" style="display:none;"> 277 <div class="gu-cb-grid"> 278 <!-- LEFT: builder --> 279 <div class="gu-cb-form"> 280 <div class="gu-cb-field"> 281 <label><?php esc_html_e( 'Template name', 'goultra-for-woocommerce' ); ?></label> 282 <input type="text" id="gu-cb-name" dir="ltr" maxlength="60" placeholder="my_offer" /> 283 <p class="gu-cb-hint"><?php esc_html_e( 'Lowercase letters, numbers and underscores only.', 'goultra-for-woocommerce' ); ?></p> 284 </div> 285 286 <div class="gu-cb-field gu-cb-prod"> 287 <label><?php esc_html_e( 'Building a template for a product?', 'goultra-for-woocommerce' ); ?> <span class="gu-cb-opt"><?php esc_html_e( '(optional)', 'goultra-for-woocommerce' ); ?></span></label> 288 <p class="gu-cb-hint"><?php esc_html_e( 'Pick a store product and we set up the template for it: an image header plus a "View product" button that links to it. Then choose a language and submit for approval.', 'goultra-for-woocommerce' ); ?></p> 289 <button type="button" class="gu-btn gu-btn-o" id="gu-cb-prod-btn" style="padding:7px 14px;font-size:12px;"><?php esc_html_e( 'Pick a product', 'goultra-for-woocommerce' ); ?></button> 290 <div id="gu-cb-prod-box" style="display:none;margin-top:8px;"> 291 <input type="search" id="gu-cb-prod-q" placeholder="<?php esc_attr_e( 'Search products', 'goultra-for-woocommerce' ); ?>" /> 292 <div id="gu-cb-prod-list" class="gu-camp-products"></div> 293 </div> 294 <div id="gu-cb-prod-chosen" class="gu-camp-chosen" style="display:none;"> 295 <img id="gu-cb-prod-thumb" src="" alt="" /> 296 <span class="gu-camp-chosen-txt" id="gu-cb-prod-name"></span> 297 <button type="button" class="gu-camp-chosen-x" id="gu-cb-prod-remove"><?php esc_html_e( 'Remove', 'goultra-for-woocommerce' ); ?></button> 298 </div> 299 </div> 300 301 <div class="gu-cb-row2"> 302 <div class="gu-cb-field"> 303 <label><?php esc_html_e( 'Category', 'goultra-for-woocommerce' ); ?></label> 304 <select id="gu-cb-category"> 305 <option value="MARKETING"><?php esc_html_e( 'Marketing (offers, promotions)', 'goultra-for-woocommerce' ); ?></option> 306 <option value="UTILITY"><?php esc_html_e( 'Utility (order, account updates)', 'goultra-for-woocommerce' ); ?></option> 232 307 </select> 233 308 </div> 234 <div style="flex:1;"> 235 <label style="font-size:13px;font-weight:600;color:#374151;display:block;margin-bottom:6px;"><?php esc_html_e( 'Header', 'goultra-for-woocommerce' ); ?></label> 236 <select id="gu-custom-header" style="width:100%;padding:10px 32px 10px 14px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:14px;-webkit-appearance:none;appearance:none;background:white url('data:image/svg+xml;utf8,<svg xmlns=%22http://www.w3.org/2000/svg%22 width=%2212%22 height=%2212%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22%236b7280%22 stroke-width=%222%22><polyline points=%226 9 12 15 18 9%22/></svg>') no-repeat right 12px center;" > 237 <option value="none"><?php esc_html_e( 'None', 'goultra-for-woocommerce' ); ?></option> 238 <option value="image"><?php esc_html_e( 'Image (default placeholder)', 'goultra-for-woocommerce' ); ?></option> 309 <div class="gu-cb-field"> 310 <label><?php esc_html_e( 'Header', 'goultra-for-woocommerce' ); ?> <span class="gu-cb-opt"><?php esc_html_e( '(optional)', 'goultra-for-woocommerce' ); ?></span></label> 311 <select id="gu-cb-header"> 312 <option value="none"><?php esc_html_e( 'No header', 'goultra-for-woocommerce' ); ?></option> 313 <option value="TEXT"><?php esc_html_e( 'Text', 'goultra-for-woocommerce' ); ?></option> 314 <option value="IMAGE"><?php esc_html_e( 'Image', 'goultra-for-woocommerce' ); ?></option> 315 <option value="VIDEO"><?php esc_html_e( 'Video', 'goultra-for-woocommerce' ); ?></option> 316 <option value="DOCUMENT"><?php esc_html_e( 'Document', 'goultra-for-woocommerce' ); ?></option> 239 317 </select> 240 318 </div> 241 319 </div> 242 <div style="margin-bottom:14px;"> 243 <label style="font-size:13px;font-weight:600;color:#374151;display:block;margin-bottom:6px;"><?php esc_html_e( 'Body Text', 'goultra-for-woocommerce' ); ?></label> 244 <textarea id="gu-custom-body" rows="6" dir="ltr" style="width:100%;padding:14px;border:1.5px solid #e5e7eb;border-radius:10px;font-size:14px;line-height:1.6;resize:vertical;font-family:inherit;" ></textarea> 245 </div> 246 <div style="display:flex;gap:12px;margin-bottom:20px;"> 247 <div style="flex:1;"> 248 <label style="font-size:13px;font-weight:600;color:#374151;display:block;margin-bottom:6px;"><?php esc_html_e( 'Button Text', 'goultra-for-woocommerce' ); ?> <span style="font-weight:400;color:#9ca3af;"><?php esc_html_e( '(optional)', 'goultra-for-woocommerce' ); ?></span></label> 249 <input type="text" id="gu-custom-btn-text" placeholder="<?php esc_attr_e( 'e.g. View Order', 'goultra-for-woocommerce' ); ?>" style="width:100%;padding:8px 12px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:14px;" /> 320 <div class="gu-cb-field" id="gu-cb-header-text-wrap" style="display:none;"> 321 <label><?php esc_html_e( 'Header text', 'goultra-for-woocommerce' ); ?></label> 322 <input type="text" id="gu-cb-header-text" maxlength="60" /> 323 </div> 324 <p class="gu-cb-hint" id="gu-cb-header-media-note" style="display:none;"><?php esc_html_e( 'You will choose the actual image/video/file when you send a campaign.', 'goultra-for-woocommerce' ); ?></p> 325 326 <div id="gu-cb-util-warn" class="gu-cb-warn" style="display:none;"> 327 <strong><?php esc_html_e( 'Note:', 'goultra-for-woocommerce' ); ?></strong> <?php esc_html_e( 'Utility templates are for transactional messages only (order, shipping, account). Promotional content will be rejected by Meta.', 'goultra-for-woocommerce' ); ?> 328 </div> 329 330 <div class="gu-cb-field"> 331 <label><?php esc_html_e( 'Insert a variable', 'goultra-for-woocommerce' ); ?> <span class="gu-cb-opt"><?php esc_html_e( '(filled in automatically at send time)', 'goultra-for-woocommerce' ); ?></span></label> 332 <div id="gu-var-chips" class="gu-cb-chips"></div> 333 </div> 334 <div id="gu-adj-warn" class="gu-cb-warn gu-cb-warn-amber" style="display:none;"> 335 <?php echo wp_kses( __( '<strong>Warning:</strong> two variables next to each other will be rejected by Meta. Add text between them.', 'goultra-for-woocommerce' ), array( 'strong' => array() ) ); ?> 336 </div> 337 338 <div class="gu-cb-field"> 339 <label><?php esc_html_e( 'Message text', 'goultra-for-woocommerce' ); ?></label> 340 <textarea id="gu-custom-body" rows="6" dir="ltr"></textarea> 341 </div> 342 343 <!-- Buttons repeater --> 344 <div class="gu-cb-field"> 345 <label><?php esc_html_e( 'Buttons', 'goultra-for-woocommerce' ); ?> <span class="gu-cb-opt"><?php esc_html_e( '(up to 10, optional)', 'goultra-for-woocommerce' ); ?></span></label> 346 <div id="gu-cb-buttons"></div> 347 <button type="button" class="gu-btn gu-btn-o" id="gu-cb-add-btn" style="margin-top:6px;padding:7px 14px;font-size:12px;">+ <?php esc_html_e( 'Add button', 'goultra-for-woocommerce' ); ?></button> 348 </div> 349 350 <!-- Opt-out (marketing only) --> 351 <div id="gu-cb-optout" class="gu-cb-optout" style="display:none;"> 352 <label><?php esc_html_e( 'Opt-out method (required for marketing)', 'goultra-for-woocommerce' ); ?></label> 353 <label class="gu-cb-radio"><input type="radio" name="gu-cb-opt" id="gu-cb-opt-footer" value="footer_text" checked /> <?php esc_html_e( 'Footer text (recommended)', 'goultra-for-woocommerce' ); ?></label> 354 <label class="gu-cb-radio"><input type="radio" name="gu-cb-opt" id="gu-cb-opt-btn" value="opt_out_button" /> <?php esc_html_e( 'Unsubscribe button', 'goultra-for-woocommerce' ); ?></label> 355 </div> 356 357 <div class="gu-cb-field"> 358 <label><?php esc_html_e( 'Footer', 'goultra-for-woocommerce' ); ?> <span class="gu-cb-opt"><?php esc_html_e( '(small gray line, optional)', 'goultra-for-woocommerce' ); ?></span></label> 359 <input type="text" id="gu-cb-footer" maxlength="60" /> 360 </div> 361 362 <button type="button" id="gu-custom-submit" class="gu-btn gu-btn-cy" style="width:100%;padding:13px;font-size:14px;"><?php esc_html_e( 'Submit to Meta for Approval', 'goultra-for-woocommerce' ); ?></button> 363 <div id="gu-custom-msg" class="gu-cb-msg" style="display:none;"></div> 364 </div> 365 366 <!-- RIGHT: WhatsApp preview --> 367 <div class="gu-cb-preview"> 368 <div class="gu-camp-phone" id="gu-cb-phone" dir="<?php echo $gu_rtl ? 'rtl' : 'ltr'; ?>"> 369 <div class="gu-camp-phone-top"> 370 <?php if ( $gu_wa_avatar ) : ?> 371 <img class="gu-camp-avatar-img" src="<?php echo esc_url( $gu_wa_avatar ); ?>" data-fallback="<?php echo esc_url( $gu_wa_fallback ); ?>" alt="" /> 372 <?php endif; ?> 373 <span class="gu-camp-avatar"<?php echo $gu_wa_avatar ? ' style="display:none"' : ''; ?>><?php echo esc_html( $gu_wa_initial ); ?></span> 374 <span class="gu-camp-phone-id"><b><?php echo esc_html( $gu_wa_name ); ?></b><small><?php esc_html_e( 'online', 'goultra-for-woocommerce' ); ?></small></span> 375 <span class="gu-camp-phone-dots" aria-hidden="true"><svg width="16" height="16" viewBox="0 0 24 24" fill="#fff" opacity=".85"><circle cx="12" cy="5" r="1.8"/><circle cx="12" cy="12" r="1.8"/><circle cx="12" cy="19" r="1.8"/></svg></span> 250 376 </div> 251 <div style="flex:2;"> 252 <label style="font-size:13px;font-weight:600;color:#374151;display:block;margin-bottom:6px;"><?php esc_html_e( 'Button URL', 'goultra-for-woocommerce' ); ?> <span style="font-weight:400;color:#9ca3af;"><?php esc_html_e( '(optional)', 'goultra-for-woocommerce' ); ?></span></label> 253 <input type="text" id="gu-custom-btn-url" placeholder="{{order_url}} or {{checkout_url}}" style="width:100%;padding:8px 12px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:14px;" /> 254 </div> 255 </div> 256 <button type="button" id="gu-custom-submit" style="padding:14px 32px;background:#0f172a;color:white;border:none;border-radius:10px;font-size:15px;font-weight:600;cursor:pointer;transition:all 0.2s;width:100%;"><?php esc_html_e( 'Submit to Meta for Approval', 'goultra-for-woocommerce' ); ?></button> 257 <div id="gu-custom-msg" style="display:none;margin-top:12px;padding:12px 16px;border-radius:8px;font-size:14px;"></div> 258 <input type="hidden" id="gu-custom-footer" value="" /> 259 </div> 260 <!-- RIGHT: Phone Preview (sticky) --> 261 <div style="flex:0 0 340px;position:sticky;top:40px;align-self:flex-start;"> 262 <div id="gu-custom-phone" style="width:320px;min-height:420px;background:#0b141a;border-radius:28px;padding:14px;box-shadow:0 8px 30px rgba(0,0,0,0.3);"> 263 <div style="display:flex;align-items:center;gap:10px;padding:10px 12px;margin-bottom:10px;"> 264 <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2"><path d="M19 12H5M12 19l-7-7 7-7"/></svg> 265 <div style="width:32px;height:32px;background:#25D366;border-radius:50%;"></div> 266 <div><div id="gu-prev-name" style="color:white;font-size:14px;font-weight:600;">Your Store</div><div style="color:#8696a0;font-size:11px;">online</div></div> 267 </div> 268 <div style="background:#0b141a;border-radius:12px;padding:10px;min-height:300px;"> 269 <div style="text-align:center;margin-bottom:10px;"><span style="background:#1a2733;color:#8696a0;padding:4px 12px;border-radius:8px;font-size:11px;">TODAY</span></div> 270 <div id="gu-prev-bubble" style="background:#1a2733;border-radius:0 12px 12px 12px;padding:0;max-width:95%;margin-left:0;"> 271 <div id="gu-prev-header" style="display:none;border-radius:12px 12px 0 0;overflow:hidden;"><img src="<?php echo esc_url(GOULTRA_WC_PLUGIN_URL . 'assets/images/woo-order-placeholder.jpg'); ?>" style="width:100%;display:block;" /></div> 272 <div id="gu-prev-body" style="padding:12px 14px;color:#e9edef;font-size:14px;line-height:1.6;white-space:pre-wrap;word-wrap:break-word;min-height:40px;direction:rtl;text-align:right;"></div> 273 <div style="padding:2px 14px 8px;display:flex;justify-content:space-between;align-items:center;"> 274 <span style="color:#8696a0;font-size:10px;"></span> 275 <span style="color:#8696a0;font-size:10px;" id="gu-prev-time"></span> 276 </div> 277 </div> 278 <div id="gu-prev-button" style="display:none;text-align:center;margin-top:6px;padding:10px;background:#1a2733;border-radius:10px;"><a style="color:#53bdeb;font-size:14px;text-decoration:none;display:flex;align-items:center;justify-content:center;gap:5px;"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="#53bdeb" stroke-width="2"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg> <span id="gu-prev-btn-text"></span></a></div> 377 <div class="gu-camp-phone-bg"> 378 <div class="gu-camp-day"><?php esc_html_e( 'Today', 'goultra-for-woocommerce' ); ?></div> 379 <div class="gu-camp-bubble" id="gu-cb-bubble"></div> 279 380 </div> 280 381 </div> … … 289 390 var GU=window.goUltraAdmin||{}; 290 391 var selType='',selLang=''; 392 // Always open this screen at the TOP. Reloads (the Sync Status button, a reload 393 // after submitting a template) made the browser RESTORE the previous deep scroll 394 // offset onto the short step-1 view, which rendered as a blank white page until 395 // the user scrolled up. Disable scroll restoration here and pin to the top; every 396 // step transition below also re-pins via guTop(). 397 if('scrollRestoration' in history){ try{ history.scrollRestoration='manual'; }catch(e){} } 398 function guTop(){ window.scrollTo(0,0); } 399 guTop(); 400 // Focus without yanking the viewport down to the input. 401 function guFocus(id){ var el=document.getElementById(id); if(!el) return; try{ el.focus({preventScroll:true}); }catch(e){ el.focus(); } } 291 402 var RTL=['ar','he','fa','ur']; 292 403 var typeMeta=<?php echo wp_json_encode($type_meta); ?>; … … 307 418 approved:<?php echo wp_json_encode( __( 'approved', 'goultra-for-woocommerce' ) ); ?>, 308 419 pending:<?php echo wp_json_encode( __( 'pending', 'goultra-for-woocommerce' ) ); ?>, 420 rejected2:<?php echo wp_json_encode( __( 'rejected', 'goultra-for-woocommerce' ) ); ?>, 421 view:<?php echo wp_json_encode( __( 'View', 'goultra-for-woocommerce' ) ); ?>, 422 noMatches:<?php echo wp_json_encode( __( 'No templates match your search.', 'goultra-for-woocommerce' ) ); ?>, 309 423 saving:<?php echo wp_json_encode( __( 'Saving...', 'goultra-for-woocommerce' ) ); ?>, 310 424 submitting:<?php echo wp_json_encode( __( 'Submitting...', 'goultra-for-woocommerce' ) ); ?>, … … 321 435 error:<?php echo wp_json_encode( __( 'Error', 'goultra-for-woocommerce' ) ); ?>, 322 436 pickStyle:<?php echo wp_json_encode( __( 'Pick a template style', 'goultra-for-woocommerce' ) ); ?>, 323 writeOwn:<?php echo wp_json_encode( __( 'Write your own template', 'goultra-for-woocommerce' ) ); ?> 437 writeOwn:<?php echo wp_json_encode( __( 'Write your own template', 'goultra-for-woocommerce' ) ); ?>, 438 btnQr:<?php echo wp_json_encode( __( 'Quick reply', 'goultra-for-woocommerce' ) ); ?>, 439 btnUrl:<?php echo wp_json_encode( __( 'URL link', 'goultra-for-woocommerce' ) ); ?>, 440 btnPhone:<?php echo wp_json_encode( __( 'Phone number', 'goultra-for-woocommerce' ) ); ?>, 441 btnText:<?php echo wp_json_encode( __( 'Button text', 'goultra-for-woocommerce' ) ); ?>, 442 btnValue:<?php echo wp_json_encode( __( 'A URL or phone button needs a value.', 'goultra-for-woocommerce' ) ); ?>, 443 remove:<?php echo wp_json_encode( __( 'Remove', 'goultra-for-woocommerce' ) ); ?>, 444 adjacent:<?php echo wp_json_encode( __( 'Two variables cannot be next to each other. Add some text between them (Meta rejects it otherwise).', 'goultra-for-woocommerce' ) ); ?>, 445 viewProduct:<?php echo wp_json_encode( __( 'View product', 'goultra-for-woocommerce' ) ); ?>, 446 loadingProd:<?php echo wp_json_encode( __( 'Loading...', 'goultra-for-woocommerce' ) ); ?>, 447 noProducts:<?php echo wp_json_encode( __( 'No products found.', 'goultra-for-woocommerce' ) ); ?> 324 448 }; 325 449 … … 406 530 var rows = (allTpls[selType] && allTpls[selType][selLang]) || []; 407 531 var thisKey = v.key || ''; 408 var matched = null; 532 // When SEVERAL rows match one style (a rejected template plus its resubmission - 533 // Meta keeps the rejected one, so a sync brings both back), the card must show 534 // the BEST one: approved > pending > rejected, and the newest within a status. 535 var stPri = {approved:3, pending:2, rejected:1}; 536 var matched = null, mScore = -1; 537 var consider = function(r, ri){ 538 var sc = (stPri[r.status] || 0) * 1000 + ri; 539 if(sc > mScore){ mScore = sc; matched = r; } 540 }; 409 541 for(var ri=0; ri<rows.length; ri++){ 410 542 // Match by style_key column first 411 if(rows[ri].style_key && rows[ri].style_key === thisKey){ matched = rows[ri]; break; } 412 // Fallback: extract style from template_name (woo_{type}_{style}_{lang}_{ts}) 543 if(rows[ri].style_key && rows[ri].style_key === thisKey){ consider(rows[ri], ri); continue; } 544 // Fallback: match the FULL woo_{type}_{style}_ name prefix. (A bare 545 // indexOf('_sale_') would also hit flash_sale names, so anchor at 0.) 413 546 var tn = rows[ri].template_name || ''; 414 if(thisKey && tn.indexOf('_' + thisKey + '_') !== -1){ matched = rows[ri]; break; } 547 if(thisKey && tn.indexOf('woo_' + selType + '_' + thisKey + '_') === 0){ consider(rows[ri], ri); } 548 } 549 // Older marketing templates were named WITHOUT a style segment 550 // (woo_marketing_offer_{lang}_{ts}), so the per-style match above cannot find 551 // them. Identify the style they were built from by comparing the stored body 552 // against this variant's preset text (variables + whitespace stripped), so the 553 // Active/Pending state shows on exactly ONE card - the same behaviour as the 554 // standard template types. 555 if(!matched && selType==='marketing_offer'){ 556 // Strip variables, bidi control marks (the backend adds RTL marks to 557 // Hebrew/Arabic bodies) and all whitespace before comparing. 558 var normB=function(s){return String(s||'').replace(/\{\{[^}]*\}\}/g,'').replace(/[\u200e\u200f\u2066-\u2069\u202a-\u202e]/g,'').replace(/\s+/g,'');}; 559 var vNorm=normB(v.tpl); 560 if(vNorm){ 561 for(var bi=0; bi<rows.length; bi++){ 562 var sk=rows[bi].style_key||''; 563 if(sk && sk!=='custom') continue; // has a real style -> matched on its own card above 564 if(normB(rows[bi].body_text)===vNorm){ consider(rows[bi], bi); } 565 } 566 } 415 567 } 416 568 … … 444 596 function renderCustomList(){ 445 597 var $list=$('#gu-custom-list').empty(); 598 var q=($('#gu-custom-search').val()||'').toLowerCase().trim(); 446 599 if(!customTemplates.length){ 600 $('#gu-custom-search').hide(); 447 601 $list.html('<div style="text-align:center;padding:40px;color:#6b7280;font-size:15px;"><p>'+esc(guT.noCustom)+'</p><p style="font-size:13px;color:#9ca3af;margin-top:8px;">'+esc(guT.noCustomHint)+'</p></div>'); 448 602 return; 449 603 } 604 $('#gu-custom-search').show(); 605 var rows=customTemplates.filter(function(t){ 606 if(!q) return true; 607 var hay=((t.template_name||'')+' '+(t.body_text||'')+' '+(langNames[t.language]||t.language||'')+' '+(t.status||'')).toLowerCase(); 608 return hay.indexOf(q)!==-1; 609 }); 610 if(!rows.length){ 611 $list.html('<div style="text-align:center;padding:34px;color:#9ca3af;font-size:14px;">'+esc(guT.noMatches)+'</div>'); 612 return; 613 } 450 614 var html='<div style="display:grid;gap:10px;">'; 451 customTemplates.forEach(function(t){ 615 rows.forEach(function(t){ 616 var idx=customTemplates.indexOf(t); 452 617 var st=t.status||'draft'; 453 618 var badgeBg=st==='approved'?'#dcfce7':st==='pending'?'#fef9c3':st==='rejected'?'#fee2e2':'#f1f5f9'; 454 619 var badgeColor=st==='approved'?'#15803d':st==='pending'?'#92400e':st==='rejected'?'#b91c1c':'#475569'; 455 620 var badgeIcon=st==='approved'?'✓':st==='pending'?'⏳':st==='rejected'?'✗':'✎'; 621 var stLabel=(st==='approved'?guT.approved:st==='pending'?guT.pending:st==='rejected'?guT.rejected2:st); 456 622 var langLabel=langNames[t.language]||t.language; 457 623 var preview=(t.body_text||'').substring(0,60)+'…'; … … 460 626 html+='<div style="width:40px;height:40px;background:'+badgeBg+';border-radius:10px;display:flex;align-items:center;justify-content:center;font-size:16px;flex-shrink:0;">'+badgeIcon+'</div>'; 461 627 html+='<div style="flex:1;min-width:0;">'; 462 html+='<div style="font-size:14px;font-weight:600;color:#1e293b;">'+ langLabel+'</div>';628 html+='<div style="font-size:14px;font-weight:600;color:#1e293b;">'+esc(langLabel)+'</div>'; 463 629 html+='<div style="font-size:12px;color:#6b7280;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">'+$('<span>').text(preview).html()+'</div>'; 464 630 html+='<div style="font-size:11px;color:#94a3b8;margin-top:2px;">'+$('<span>').text(metaName).html()+'</div>'; 465 631 html+='</div>'; 466 html+='<span style="padding:4px 10px;background:'+badgeBg+';color:'+badgeColor+';border-radius:6px;font-size:12px;font-weight:600;text-transform:capitalize;">'+st+'</span>'; 632 html+='<button type="button" class="gu-tpl-view gu-btn gu-btn-o gu-camp-sm" data-idx="'+idx+'">'+esc(guT.view)+'</button>'; 633 html+='<span style="padding:4px 10px;background:'+badgeBg+';color:'+badgeColor+';border-radius:6px;font-size:12px;font-weight:600;">'+esc(stLabel)+'</span>'; 467 634 html+='</div>'; 468 635 }); … … 470 637 $list.html(html); 471 638 } 639 640 // WhatsApp-style preview of one saved custom template (read-only modal). 641 function showTplModal(t){ 642 if(!t) return; 643 var base=String(t.language||'en').split('_')[0]; 644 var isRtl=(base==='he'||base==='ar'||base==='fa'||base==='ur'); 645 var sd=getSamples(t.language||'en'); 646 var body=fill(String(t.body_text||''),sd); 647 var ht=String(t.header_type||'none').toLowerCase(); 648 var h='<div class="gu-camp-phone" dir="'+(isRtl?'rtl':'ltr')+'" style="max-width:360px;margin:6px auto 0;">'; 649 h+='<div class="gu-camp-phone-top"><span class="gu-camp-avatar">'+esc((storeName||'S').charAt(0).toUpperCase())+'</span><div class="gu-camp-phone-id"><b>'+esc(storeName)+'</b><small>WhatsApp Business</small></div></div>'; 650 h+='<div class="gu-camp-phone-bg"><div class="gu-camp-bubble">'; 651 if(ht==='image') h+=cbMediaBox(CB_ICO.img); 652 else if(ht==='video') h+=cbMediaBox(CB_ICO.vid); 653 else if(ht==='document') h+=cbMediaBox(CB_ICO.doc); 654 else if(ht==='text' && t.header_text) h+='<div class="gu-camp-b-htext">'+esc(t.header_text)+'</div>'; 655 h+='<div class="gu-camp-b-body">'+$('<span>').text(body).html().replace(/\n/g,'<br>')+'</div>'; 656 if(t.footer_text) h+='<div class="gu-camp-b-footer">'+esc(t.footer_text)+'</div>'; 657 h+='<div class="gu-camp-b-time">10:30 <span style="color:#53bdeb;">✓✓</span></div>'; 658 h+='</div>'; 659 if(t.button_text) h+='<div class="gu-camp-b-btn">'+esc(t.button_text)+'</div>'; 660 h+='</div></div>'; 661 var metaName=t.template_name||('woo_'+t.template_key+'_'+t.language); 662 h+='<div style="margin-top:12px;font-size:11px;color:#94a3b8;text-align:center;word-break:break-all;">'+esc(metaName)+'</div>'; 663 $('#gu-tpl-modal-body').html(h); 664 $('#gu-tpl-modal').css('display','flex'); 665 } 666 $(document).on('click','.gu-tpl-view',function(){ showTplModal(customTemplates[parseInt($(this).data('idx'),10)]); }); 667 $('#gu-tpl-modal-x, #gu-tpl-modal').on('click',function(e){ if(e.target===this) $('#gu-tpl-modal').hide(); }); 668 $(document).on('keydown',function(e){ if(e.key==='Escape') $('#gu-tpl-modal').hide(); }); 669 $('#gu-custom-search').on('input',renderCustomList); 472 670 473 671 // Sync from GoUltra and reload page … … 484 682 // Go to Custom Templates management page 485 683 renderCustomList(); 486 $('#gu-s1').hide();$('#gu-s4').show(); 684 $('#gu-s1').hide();$('#gu-s4').show();guTop(); 487 685 return; 488 686 } 489 687 var m=typeMeta[selType]||{}; 490 688 $('#gu-s2t').text((typeLabels[selType]||'')); 491 $('#gu-s1').hide();$('#gu-s2').show();$('#gu-ls').val('').trigger('input') .focus();689 $('#gu-s1').hide();$('#gu-s2').show();$('#gu-ls').val('').trigger('input');guFocus('gu-ls');guTop(); 492 690 }); 493 691 … … 496 694 selType='custom'; 497 695 $('#gu-s2t').text('Custom Template'); 498 $('#gu-s4').hide();$('#gu-s2').show();$('#gu-ls').val('').trigger('input') .focus();696 $('#gu-s4').hide();$('#gu-s2').show();$('#gu-ls').val('').trigger('input');guFocus('gu-ls');guTop(); 499 697 }); 500 698 … … 507 705 var isRtl=RTL.indexOf(base)!==-1; 508 706 509 if (selType === 'custom') { 510 // Show custom editor 511 $('#gu-ph').hide(); 512 $('#gu-s3sub').text(guT.writeOwn); 513 $('#gu-custom-editor').show(); 514 // RTL for Hebrew/Arabic 515 var dir=isRtl?'rtl':'ltr'; 516 var align=isRtl?'right':'left'; 517 $('#gu-custom-body').attr('dir',dir).css('text-align',align); 518 $('#gu-prev-body').css({'direction':dir,'text-align':align}); 519 // Placeholder by language 520 var phText=isRtl?(base==='he'?'שלום {{full_name}}, ההזמנה שלך מספר #{{order_number}} התקבלה בהצלחה!':'مرحباً {{full_name}}، تم استلام طلبك رقم #{{order_number}} بنجاح!'):'Hi {{full_name}}, your order #{{order_number}} has been received!'; 521 $('#gu-custom-body').attr('placeholder',phText); 522 // Time for preview 523 var d=new Date();$('#gu-prev-time').text(('0'+d.getHours()).slice(-2)+':'+('0'+d.getMinutes()).slice(-2)); 524 // Track which field was last focused 525 var lastFocusedField = 'gu-custom-body'; 526 $('#gu-custom-body, #gu-custom-btn-url').on('focus', function(){ lastFocusedField = this.id; }); 527 528 // Variable chips - full_name + first_name + last_name all available 529 var vars=['full_name','first_name','last_name','order_id','order_number','order_total','currency','items_summary','order_url','checkout_url','tracking_url','payment_method','coupon_code','discount_percent']; 530 var $chips=$('#gu-var-chips').empty(); 531 vars.forEach(function(v){ 532 $('<span style="padding:4px 10px;background:#eff6ff;color:#1d4ed8;border-radius:6px;font-size:12px;font-weight:500;cursor:pointer;border:1px solid #bfdbfe;transition:all 0.15s;" onmouseover="this.style.background=\'#dbeafe\'" onmouseout="this.style.background=\'#eff6ff\'">{{'+v+'}}</span>') 533 .on('click',function(){ 534 var ta=document.getElementById(lastFocusedField); 535 var pos=ta.selectionStart||ta.value.length; 536 ta.value=ta.value.substring(0,pos)+'{{'+v+'}}'+ta.value.substring(pos); 537 ta.focus();ta.selectionStart=ta.selectionEnd=pos+v.length+4; 538 checkAdjacentVars();updateCustomPreview(); 539 }).appendTo($chips); 540 }); 541 $('#gu-custom-body').off('input.adj').on('input.adj',function(){checkAdjacentVars();updateCustomPreview();}); 542 function checkAdjacentVars(){ 543 var t=$('#gu-custom-body').val(); 544 $('#gu-adj-warn').toggle(/\}\}\s*\{\{/.test(t)); 545 } 546 updateCustomPreview(); 547 } else { 548 // Standard flow 549 $('#gu-custom-editor').hide(); 550 $('#gu-ph').show(); 551 $('#gu-s3sub').text(guT.pickStyle); 552 553 // Compute status from allTpls and show banner 554 var rows = (allTpls[selType] && allTpls[selType][selLang]) || []; 707 // Status banner for step 3: counts + names of approved/pending templates of 708 // THIS area + language (translated words). Shared by the standard styles view 709 // and the custom builder, so the custom screen never shows a stale banner that 710 // belongs to a different template type. 711 function showStatusBanner(rows){ 555 712 var nApproved=0, nPending=0, approvedNames=[], pendingNames=[]; 556 713 for(var si=0;si<rows.length;si++){ … … 558 715 if(rows[si].status==='pending'){ nPending++; if(rows[si].template_name) pendingNames.push(rows[si].template_name); } 559 716 } 560 561 717 var $banner=$('#gu-status-banner').hide().empty(); 562 718 if(nApproved > 0 || nPending > 0){ 563 719 var html='<div style="display:flex;align-items:center;gap:10px;flex-wrap:wrap;">'; 564 if(nApproved > 0) html+='<span style="padding:5px 12px;background:#dcfce7;color:#15803d;border-radius:6px;font-weight:600;font-size:13px;">✓ '+nApproved+' approved</span>';565 if(nPending > 0) html+='<span style="padding:5px 12px;background:#fef9c3;color:#92400e;border-radius:6px;font-weight:600;font-size:13px;">⏳ '+nPending+' pending</span>';720 if(nApproved > 0) html+='<span style="padding:5px 12px;background:#dcfce7;color:#15803d;border-radius:6px;font-weight:600;font-size:13px;">✓ '+nApproved+' '+esc(guT.approved)+'</span>'; 721 if(nPending > 0) html+='<span style="padding:5px 12px;background:#fef9c3;color:#92400e;border-radius:6px;font-weight:600;font-size:13px;">⏳ '+nPending+' '+esc(guT.pending)+'</span>'; 566 722 html+='</div>'; 567 723 if(approvedNames.length) html+='<div style="margin-top:6px;font-size:11px;color:#6b7280;">'+approvedNames.map(esc).join(', ')+'</div>'; … … 571 727 $banner.html(html).css({background:bg,border:'1.5px solid '+border}).show(); 572 728 } 729 } 730 731 if (selType === 'custom') { 732 $('#gu-ph').hide(); 733 $('#gu-s3sub').text(guT.writeOwn); 734 // The banner must reflect CUSTOM templates of this language only - not a 735 // marketing/standard template from a previously viewed screen. 736 var cbase=selLang.split('_')[0]; 737 showStatusBanner(customTemplates.filter(function(t){ 738 var tl=String(t.language||''); 739 return tl===selLang || tl.split('_')[0]===cbase; 740 })); 741 $('#gu-custom-editor').show(); 742 setupCustomBuilder(selLang, isRtl); 743 } else { 744 // Standard flow 745 $('#gu-custom-editor').hide(); 746 $('#gu-ph').show(); 747 $('#gu-s3sub').text(guT.pickStyle); 748 749 // Compute status from allTpls and show banner 750 showStatusBanner((allTpls[selType] && allTpls[selType][selLang]) || []); 573 751 574 752 var fk=fontKey(selLang); … … 583 761 } 584 762 585 $('#gu-s2').hide();$('#gu-s3').show(); 763 $('#gu-s2').hide();$('#gu-s3').show();guTop(); 586 764 setTimeout(function(){ 587 765 var maxH=0; … … 591 769 }); 592 770 593 // Live preview for custom editor 594 window.updateCustomPreview=function(){ 771 // ===== Rich custom / marketing template builder ===== 772 var CB_BUTTONS = []; 773 var CB_OPTFOOTER = { en:'Reply STOP to unsubscribe', he:'להסרה השב הסר', ar:'للإزالة أرسل قف', es:'Responda PARAR para darse de baja', fr:'Répondez STOP pour vous désinscrire', de:'Antworten Sie STOPP zum Abbestellen', it:'Rispondi STOP per disiscriverti', pt:'Responda PARAR para cancelar', ru:'Ответьте СТОП для отписки', tr:'Aboneliği iptal etmek için DUR yazın', nl:'Antwoord STOP om af te melden', pl:'Wyślij STOP aby zrezygnować', uk:'Відповідайте СТОП для відписки', id:'Balas STOP untuk berhenti berlangganan', ms:'Balas STOP untuk berhenti melanggan', vi:'Trả lời STOP để hủy đăng ký', th:'ตอบ STOP เพื่อยกเลิกการสมัคร', ja:'配信停止には STOP と返信してください', ko:'구독 취소하려면 STOP 회신', zh:'回复 STOP 取消订阅', hi:'सदस्यता रद्द करने के लिए STOP जवाब दें' }; 774 var CB_OPTBTN = { en:'Stop promotions', he:'הסרה מהרשימה', ar:'إيقاف الرسائل', fr:'Se désabonner', es:'Dejar de recibir' }; 775 776 // Avatar: the WhatsApp profile picture is served from a signed URL that 777 // expires after ~15 minutes. If it fails to load, fall back to the site icon 778 // and then to the initial letter, so the preview never shows a broken image. 779 $('.gu-camp-avatar-img').each(function(){ 780 var img=this, $img=$(img); 781 var fail=function(){ 782 var fb=$img.data('fallback'); 783 if(fb && img.getAttribute('src')!==fb){ img.src=fb; return; } 784 $img.hide().siblings('.gu-camp-avatar').show(); 785 }; 786 img.onerror=fail; 787 if(img.complete && img.naturalWidth===0){ fail(); } 788 }); 789 790 function setupCustomBuilder(lang, isRtl){ 791 var base = lang.split('_')[0]; 792 CB_BUTTONS = []; 793 $('#gu-cb-name').val(''); 794 $('#gu-cb-category').val('MARKETING'); 795 $('#gu-cb-header').val('none'); 796 $('#gu-cb-header-text').val(''); 797 $('#gu-cb-footer').val(''); 798 $('#gu-cb-opt-footer').prop('checked', true); 799 $('#gu-cb-prod-chosen').hide(); 800 $('#gu-cb-prod-box').hide().data('loaded', 0); 801 var dir = isRtl ? 'rtl' : 'ltr', align = isRtl ? 'right' : 'left'; 802 $('#gu-custom-body').val('').attr('dir', dir).css('text-align', align) 803 .attr('placeholder', base==='he' ? 'שלום {{first_name}}! מבצע מיוחד רק בשבילך.' : (base==='ar' ? 'مرحباً {{first_name}}! عرض خاص لك.' : 'Hi {{first_name}}! A special offer just for you.')); 804 // variable chips 805 var vars=['first_name','full_name','last_name','store_name','order_id','order_number','order_total','currency','items_summary','order_url','tracking_url','checkout_url','coupon_code','discount_percent']; 806 var $chips=$('#gu-var-chips').empty(); 807 vars.forEach(function(v){ 808 $('<span class="gu-cb-chip" dir="ltr">{{'+v+'}}</span>').on('click',function(){ 809 insertVar('{{'+v+'}}'); renderCbPreview(); 810 }).appendTo($chips); 811 }); 812 renderCbButtons(); 813 cbCatToggle(); cbHeaderToggle(); 814 $('#gu-cb-bubble').closest('.gu-camp-phone').attr('dir', dir); 815 renderCbPreview(); 816 } 817 function insertVar(txt){ 818 var ta=document.getElementById('gu-custom-body'); if(!ta)return; 819 var s=ta.selectionStart||ta.value.length, e=ta.selectionEnd||s; 820 var before=ta.value.slice(0,s), after=ta.value.slice(e); 821 if(/\}\}\s*$/.test(before)) txt=' '+txt; 822 if(/^\s*\{\{/.test(after)) txt=txt+' '; 823 ta.value=before+txt+after; ta.focus(); 824 var pos=(before+txt).length; ta.selectionStart=ta.selectionEnd=pos; 825 } 826 function cbHeaderToggle(){ 827 var h=$('#gu-cb-header').val(); 828 $('#gu-cb-header-text-wrap').toggle(h==='TEXT'); 829 $('#gu-cb-header-media-note').toggle(h==='IMAGE'||h==='VIDEO'||h==='DOCUMENT'); 830 renderCbPreview(); 831 } 832 function cbCatToggle(){ 833 var mk=$('#gu-cb-category').val()==='MARKETING'; 834 $('#gu-cb-optout').toggle(mk); 835 $('#gu-cb-util-warn').toggle(!mk); 836 cbOptToggle(); 837 } 838 function cbOptToggle(){ 839 var mk=$('#gu-cb-category').val()==='MARKETING'; 840 var footerMethod=$('#gu-cb-opt-footer').is(':checked'); 841 var f=$('#gu-cb-footer'), base=selLang.split('_')[0]; 842 if(mk && footerMethod){ 843 f.val(CB_OPTFOOTER[base]||CB_OPTFOOTER.en).prop('disabled',true); 844 } else { 845 var vals=Object.keys(CB_OPTFOOTER).map(function(k){return CB_OPTFOOTER[k];}); 846 if(vals.indexOf(f.val())>=0) f.val(''); 847 f.prop('disabled',false); 848 } 849 renderCbPreview(); 850 } 851 function renderCbButtons(){ 852 var $w=$('#gu-cb-buttons').empty(); 853 CB_BUTTONS.forEach(function(b,i){ 854 var needsVal=(b.type==='URL'||b.type==='PHONE_NUMBER'); 855 var row=$('<div class="gu-cb-btnrow"></div>'); 856 var sel=$('<select class="gu-cb-bt"></select>') 857 .append('<option value="QUICK_REPLY"'+(b.type==='QUICK_REPLY'?' selected':'')+'>'+esc(guT.btnQr)+'</option>') 858 .append('<option value="URL"'+(b.type==='URL'?' selected':'')+'>'+esc(guT.btnUrl)+'</option>') 859 .append('<option value="PHONE_NUMBER"'+(b.type==='PHONE_NUMBER'?' selected':'')+'>'+esc(guT.btnPhone)+'</option>') 860 .on('change',function(){CB_BUTTONS[i].type=$(this).val();renderCbButtons();renderCbPreview();}); 861 var txt=$('<input type="text" maxlength="25" placeholder="'+esc(guT.btnText)+'" />').val(b.text||'') 862 .on('input',function(){CB_BUTTONS[i].text=$(this).val();renderCbPreview();}); 863 var rm=$('<button type="button" class="gu-cb-btnrm" title="'+esc(guT.remove)+'">✕</button>') 864 .on('click',function(){CB_BUTTONS.splice(i,1);renderCbButtons();renderCbPreview();}); 865 var top=$('<div class="gu-cb-btnrow-top"></div>').append(sel).append(txt).append(rm); 866 row.append(top); 867 if(needsVal){ 868 var val=$('<input type="text" dir="ltr" class="gu-cb-bv" placeholder="'+(b.type==='URL'?'https://':'+1 555 123 4567')+'" />').val(b.value||'') 869 .on('input',function(){CB_BUTTONS[i].value=$(this).val();renderCbPreview();}); 870 row.append(val); 871 } 872 $w.append(row); 873 }); 874 $('#gu-cb-add-btn').toggle(CB_BUTTONS.length<10); 875 } 876 $('#gu-cb-add-btn').on('click',function(){ 877 if(CB_BUTTONS.length<10){CB_BUTTONS.push({type:'QUICK_REPLY',text:'',value:''});renderCbButtons();renderCbPreview();} 878 }); 879 $('#gu-cb-header').on('change',cbHeaderToggle); 880 $('#gu-cb-header-text').on('input',renderCbPreview); 881 $('#gu-cb-category').on('change',cbCatToggle); 882 $('input[name="gu-cb-opt"]').on('change',cbOptToggle); 883 $('#gu-cb-footer').on('input',renderCbPreview); 884 $('#gu-custom-body').on('input',function(){ 885 $('#gu-adj-warn').toggle(/\}\}\s*\{\{/.test($(this).val())); 886 renderCbPreview(); 887 }); 888 $('#gu-cb-name').on('input',function(){this.value=this.value.toLowerCase().replace(/[^a-z0-9_]/g,'_');}); 889 890 // Product picker: build a marketing template for a specific store product. Picking 891 // a product sets an image header + a "View product" URL button linking to it. The 892 // product's link is baked into the template (static), so once Meta approves it the 893 // template is ready to send for that product. 894 $('#gu-cb-prod-btn').on('click',function(){ 895 var $box=$('#gu-cb-prod-box'); $box.toggle(); 896 if($box.is(':visible') && !$box.data('loaded')){ cbLoadProducts(''); $box.data('loaded',1); } 897 }); 898 var cbProdTimer; 899 $('#gu-cb-prod-q').on('input',function(){ clearTimeout(cbProdTimer); var q=this.value; cbProdTimer=setTimeout(function(){cbLoadProducts(q);},350); }); 900 function cbLoadProducts(q){ 901 $('#gu-cb-prod-list').html('<span class="gu-cb-hint">'+esc(guT.loadingProd)+'</span>'); 902 $.post(GU.ajaxUrl,{action:'goultra_camp_products',nonce:GU.nonce,q:q},function(res){ 903 var items=(res&&res.success&&res.data&&res.data.products)?res.data.products:[]; 904 if(!items.length){ $('#gu-cb-prod-list').html('<span class="gu-cb-hint">'+esc(guT.noProducts)+'</span>'); return; } 905 var html=''; 906 items.forEach(function(p){ 907 html+='<button type="button" class="gu-camp-product" data-img="'+esc(p.image)+'" data-url="'+esc(p.url||'')+'" data-title="'+esc(p.title)+'" title="'+esc(p.title)+'"><img src="'+esc(p.image)+'" alt="" /><span>'+esc(p.title)+'</span></button>'; 908 }); 909 $('#gu-cb-prod-list').html(html); 910 }); 911 } 912 $('#gu-cb-prod-list').on('click','.gu-camp-product',function(){ 913 var img=$(this).data('img'), url=String($(this).data('url')||''), title=String($(this).data('title')||''); 914 $('#gu-cb-header').val('IMAGE'); cbHeaderToggle(); 915 var idx=-1, i; 916 for(i=0;i<CB_BUTTONS.length;i++){ if(CB_BUTTONS[i].product){ idx=i; break; } } 917 var btn={type:'URL',text:guT.viewProduct,value:url,product:true}; 918 if(idx>=0){ CB_BUTTONS[idx]=btn; } else if(CB_BUTTONS.length<10){ CB_BUTTONS.push(btn); } 919 renderCbButtons(); 920 if(!$.trim($('#gu-cb-name').val())){ $('#gu-cb-name').val(('offer_'+title).toLowerCase().replace(/[^a-z0-9_]/g,'_').slice(0,50)); } 921 // Smart starter: if the message is still empty, pre-fill it with the localized 922 // "new product" starter text plus the product's title, so the owner starts 923 // from a ready draft instead of a blank box (fully editable). 924 if(!$.trim($('#gu-custom-body').val()) && title){ 925 var sBase=selLang.split('_')[0]; 926 var sVars=(localized.marketing_offer&&(localized.marketing_offer[selLang]||localized.marketing_offer[sBase]||localized.marketing_offer.en))||[]; 927 var sTpl=''; 928 for(var sv=0;sv<sVars.length;sv++){ if(sVars[sv].key==='launch'){ sTpl=sVars[sv].tpl||''; break; } } 929 if(!sTpl && sVars.length){ sTpl=sVars[0].tpl||''; } 930 if(sTpl){ $('#gu-custom-body').val(sTpl+'\n\n🛍️ '+title).trigger('input'); } 931 } 932 $('#gu-cb-prod-thumb').attr('src',img); $('#gu-cb-prod-name').text(title); $('#gu-cb-prod-chosen').css('display','flex'); 933 $('#gu-cb-prod-box').hide(); 934 renderCbPreview(); 935 }); 936 $('#gu-cb-prod-remove').on('click',function(){ 937 for(var i=CB_BUTTONS.length-1;i>=0;i--){ if(CB_BUTTONS[i].product){ CB_BUTTONS.splice(i,1); } } 938 renderCbButtons(); $('#gu-cb-prod-chosen').hide(); renderCbPreview(); 939 }); 940 941 // Clean WhatsApp-style media placeholders (SVG, not emoji) for the preview header. 942 var CB_ICO = { 943 img: '<svg viewBox="0 0 24 24" width="34" height="34" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2.5"/><circle cx="8.5" cy="8.5" r="1.7"/><path d="M21 15l-5-5L6 21"/></svg>', 944 vid: '<svg viewBox="0 0 24 24" width="34" height="34" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><rect x="2.5" y="4.5" width="19" height="15" rx="2.5"/><path d="M10 8.8l5.5 3.2L10 15.2z" fill="currentColor" stroke="none"/></svg>', 945 doc: '<svg viewBox="0 0 24 24" width="34" height="34" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8z"/><path d="M14 3v5h5"/><path d="M9 13h6M9 17h6"/></svg>' 946 }; 947 function cbMediaBox(ico){ return '<div class="gu-camp-b-media"><span class="gu-camp-b-media-ico" aria-hidden="true">'+ico+'</span></div>'; } 948 function renderCbPreview(){ 949 var sd=getSamples(selLang), base=selLang.split('_')[0], isRtl=(base==='he'||base==='ar'); 950 var html=''; 951 // header (clean WhatsApp-style media placeholder, not an emoji) 952 var h=$('#gu-cb-header').val(); 953 if(h==='IMAGE') html+=cbMediaBox(CB_ICO.img); 954 else if(h==='VIDEO') html+=cbMediaBox(CB_ICO.vid); 955 else if(h==='DOCUMENT') html+=cbMediaBox(CB_ICO.doc); 956 else if(h==='TEXT'){ var ht=$('#gu-cb-header-text').val(); if(ht) html+='<div class="gu-camp-b-htext">'+esc(ht)+'</div>'; } 957 // body with sample vars highlighted 595 958 var body=$('#gu-custom-body').val()||''; 596 var sd=getSamples(selLang); 597 // Replace variables with sample values 598 var preview=body.replace(/\{\{(\w+)\}\}/g,function(m,k){ 599 if(k==='full_name') return (sd.first_name||'John')+' '+(sd.last_name||'Smith'); 600 return sd[k]||k; 601 }); 602 $('#gu-prev-body').text(preview||' '); 603 // Header 604 var hdr=$('#gu-custom-header').val(); 605 $('#gu-prev-header').toggle(hdr==='image'); 606 // Button 607 var btnText=$('#gu-custom-btn-text').val(); 608 $('#gu-prev-button').toggle(!!btnText); 609 $('#gu-prev-btn-text').text(btnText||''); 610 }; 611 612 // Bind preview updates via jQuery (not inline handlers) 613 $(document).on('input','#gu-custom-body',function(){updateCustomPreview();}); 614 $(document).on('change','#gu-custom-header',function(){updateCustomPreview();}); 615 $(document).on('input','#gu-custom-btn-text',function(){updateCustomPreview();}); 616 617 // Custom template submit 959 var filled=esc(body).replace(/\{\{\s*([a-zA-Z_]+)\s*\}\}/g,function(m,k){ 960 k=k.toLowerCase(); 961 var v=(k==='full_name')?((sd.first_name||'Sarah')+' '+(sd.last_name||'Miller')):(sd[k]||k); 962 return '<span class="gu-camp-b-var">'+esc(v)+'</span>'; 963 }).replace(/\n/g,'<br />'); 964 html+='<div class="gu-camp-b-body" style="direction:'+(isRtl?'rtl':'ltr')+';text-align:'+(isRtl?'right':'left')+'">'+(filled||' ')+'</div>'; 965 // footer 966 var ft=$('#gu-cb-footer').val(); 967 if(ft) html+='<div class="gu-camp-b-footer">'+esc(ft)+'</div>'; 968 html+='<div class="gu-camp-b-time">10:30</div>'; 969 // buttons 970 CB_BUTTONS.forEach(function(b){ if(b.text) html+='<div class="gu-camp-b-btn">'+esc(b.text)+'</div>'; }); 971 if($('#gu-cb-category').val()==='MARKETING' && $('#gu-cb-opt-btn').is(':checked')){ 972 html+='<div class="gu-camp-b-btn">'+esc(CB_OPTBTN[base]||CB_OPTBTN.en)+'</div>'; 973 } 974 $('#gu-cb-bubble').html(html); 975 } 976 977 // Submit the rich template 618 978 $('#gu-custom-submit').on('click',function(){ 619 var body=$('#gu-custom-body').val().trim(); 620 if(!body){$('#gu-custom-msg').show().css({background:'#fee2e2',color:'#b91c1c',border:'1px solid #fecaca'}).text(guT.bodyRequired);return;} 621 // full_name is handled natively by the backend - no conversion needed 622 var metaBody = body; 979 var body=$.trim($('#gu-custom-body').val()||''); 980 var $msg=$('#gu-custom-msg'); 981 function err(t){$msg.show().removeClass('ok').addClass('bad').text(t);} 982 if(body.length<5){err(guT.bodyRequired);return;} 983 if(/\}\}\s*\{\{/.test(body)){err(guT.adjacent);return;} 984 var cat=$('#gu-cb-category').val(); 985 // validate button values 986 var bad=CB_BUTTONS.filter(function(b){return b.text&&b.text.trim();}) 987 .some(function(b){return (b.type==='URL'||b.type==='PHONE_NUMBER')&&!$.trim(b.value||'');}); 988 if(bad){err(guT.btnValue);return;} 623 989 var $btn=$(this).prop('disabled',true).text(guT.submitting); 624 var $msg=$('#gu-custom-msg').hide();990 $msg.hide(); 625 991 $.post(GU.ajaxUrl,{ 626 action:'goultra_save_template',nonce:GU.nonce, 627 template_key:'custom_'+Math.floor(Date.now()/1000),language:selLang, 628 header_type:$('#gu-custom-header').val(), 629 body_text:metaBody, 630 footer_text:'', 631 button_text:$('#gu-custom-btn-text').val(), 632 button_url:$('#gu-custom-btn-url').val() 633 },function(saveRes){ 634 if(!saveRes.success){$btn.prop('disabled',false).text(guT.submitMeta);$msg.show().css({background:'#fee2e2',color:'#b91c1c',border:'1px solid #fecaca'}).text(saveRes.data||guT.error);return;} 635 $.post(GU.ajaxUrl,{ 636 action:'goultra_submit_template',nonce:GU.nonce,id:saveRes.data.id, 637 category:$('#gu-custom-category').val() 638 },function(subRes){ 639 $btn.prop('disabled',false).text(guT.submitMeta); 640 if(subRes.success){ 641 $msg.show().css({background:'#dcfce7',color:'#15803d',border:'1px solid #bbf7d0'}).text(guT.submitted); 642 setTimeout(function(){location.reload();},2000); 643 } else { 644 $msg.show().css({background:'#fee2e2',color:'#b91c1c',border:'1px solid #fecaca'}).text(guT.savedLocally+' '+(subRes.data||guT.metaFailed)); 645 } 646 }); 647 }); 648 }); 649 650 $('#gu-b1').on('click',function(){$('#gu-s2').hide();if(selType==='custom'){$('#gu-s4').show();}else{$('#gu-s1').show();}}); 651 $('#gu-b4').on('click',function(){$('#gu-s4').hide();$('#gu-s1').show();}); 992 action:'goultra_submit_rich_template', nonce:GU.nonce, 993 language:selLang, category:cat, 994 header_type:$('#gu-cb-header').val()==='none'?'NONE':$('#gu-cb-header').val(), 995 header_text:$('#gu-cb-header-text').val(), 996 body:body, 997 footer:$('#gu-cb-footer').val(), 998 buttons:JSON.stringify(CB_BUTTONS.filter(function(b){return b.text&&b.text.trim();})), 999 opt_out_method:$('#gu-cb-opt-btn').is(':checked')?'opt_out_button':'footer_text', 1000 name:$('#gu-cb-name').val() 1001 },function(res){ 1002 $btn.prop('disabled',false).text(guT.submitMeta); 1003 if(res&&res.success){ 1004 $msg.show().removeClass('bad').addClass('ok').text(guT.submitted); 1005 setTimeout(function(){location.reload();},2200); 1006 } else { err((res&&res.data&&res.data.message)?res.data.message:guT.metaFailed); } 1007 }).fail(function(){err(guT.networkError);}); 1008 }); 1009 1010 $('#gu-b1').on('click',function(){$('#gu-s2').hide();if(selType==='custom'){$('#gu-s4').show();}else{$('#gu-s1').show();}guTop();}); 1011 $('#gu-b4').on('click',function(){$('#gu-s4').hide();$('#gu-s1').show();guTop();}); 652 1012 $('#gu-b2').on('click',function(){ 653 1013 $('#gu-s3').hide(); … … 660 1020 $('#gu-s2').show(); 661 1021 } 1022 guTop(); 662 1023 }); 663 1024 $('#gu-ls').on('input',function(){ … … 670 1031 var variants=getVariants(selType,selLang); 671 1032 var v=variants[idx];if(!v)return; 672 var $btn=$(this).prop('disabled',true).text(guT.saving); 1033 var $btn=$(this).prop('disabled',true).text(guT.submitting); 1034 1035 // Marketing offers go through the rich path (opt-out footer + full support). 1036 if(selType==='marketing_offer'){ 1037 var btns=(v.button_text)?[{type:'URL',text:v.button_text,value:v.button_url||''}]:[]; 1038 $.post(GU.ajaxUrl,{ 1039 action:'goultra_submit_rich_template', nonce:GU.nonce, 1040 language:selLang, category:'MARKETING', 1041 style_key:(v.key||''), 1042 header_type:(v.header_type==='image'?'IMAGE':'NONE'), 1043 header_text:'', body:v.tpl, 1044 footer:v.footer||'', buttons:JSON.stringify(btns), 1045 opt_out_method:'footer_text', name:'offer_'+(v.key||'') 1046 },function(res){ 1047 if(res&&res.success){alert(guT.submittedMeta);} 1048 else{alert((res&&res.data&&res.data.message)?res.data.message:guT.metaFailed);} 1049 location.reload(); 1050 }).fail(function(){alert(guT.networkError);$btn.prop('disabled',false).text(guT.useThis);}); 1051 return; 1052 } 1053 1054 // Standard utility types: the proven save + submit flow. 1055 $btn.text(guT.saving); 673 1056 $.post(GU.ajaxUrl,{ 674 1057 action:'goultra_save_template',nonce:GU.nonce, -
goultra-for-woocommerce/trunk/uninstall.php
r3588005 r3617733 27 27 $wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}goultra_templates" ); 28 28 $wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}goultra_analytics" ); 29 $wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}goultra_campaigns" ); 30 31 // Remove per-user plugin meta (admin-verification challenge tokens) 32 $wpdb->query( "DELETE FROM {$wpdb->usermeta} WHERE meta_key = '_goultra_av_token'" ); 29 33 30 34 // Remove all transients … … 47 51 'goultra_async_send', 48 52 'goultra_async_shipping', 53 'goultra_async_consent', 54 'goultra_async_redact', 49 55 ); 50 56 foreach ( $goultra_hooks as $goultra_hook ) {
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)