Plugin Directory

Changeset 3617733


Ignore:
Timestamp:
07/21/2026 09:44:56 PM (3 months ago)
Author:
goultraai
Message:

Release 3.9.24

Location:
goultra-for-woocommerce
Files:
77 added
29 edited

Legend:

Unmodified
Added
Removed
  • goultra-for-woocommerce/trunk/assets/css/admin.css

    r3603449 r3617733  
    209209    background:var(--gu-slate-lt); padding:2px 8px; border-radius:20px; vertical-align:middle;
    210210}
    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); }
    212212.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;
    215216    transition:var(--gu-trans);
    216217}
    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; }
    218220
    219221/* ── Cards ─────────────────────────────────────────────────────────────────── */
    … …  
    559561.gu-ml-import-status { font-size:12px; font-weight:700; color:var(--gu-slate); }
    560562.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); }
    563565.gu-ml-tablewrap { overflow-x:auto; }
    564566.gu-ml-table { width:100%; border-collapse:collapse; }
    … …  
    582584.gu-live-pill { background:#DCFCE7; color:#166534; border-radius:20px; padding:2px 10px; font-size:11px; font-weight:700; white-space:nowrap; }
    583585.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); }
     731tr: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  
    274274
    275275
    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: orderId
    293         }, 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 Meta
    300                 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 
    326276})(jQuery);
  • goultra-for-woocommerce/trunk/assets/js/subscribers.js

    r3603449 r3617733  
    2222        if ( src === 'woo_abandoned_cart' ) { return esc( S.i18n.cart ); }
    2323        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 );
    2437    }
    2538
    … …  
    6376                    ? '<span class="gu-ml-badge gu-ml-badge-on">' + esc( S.i18n.subscribed ) + '</span>'
    6477                    : '<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 ) );
    6679                var action = granted
    6780                    ? '<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>'
    6881                    : '';
    6982                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>'
    7189                    + '<td class="gu-ml-phone"><bdi dir="ltr">' + esc( s.phone ) + '</bdi></td>'
    7290                    + '<td>' + esc( s.email ) + '</td>'
    … …  
    149167    } );
    150168
     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 ) + '">&#10003;</button>'
     180            + '<button type="button" class="gu-ml-name-cancel" title="' + esc( S.i18n.cancel ) + '">&#10005;</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( '&#10003;' );
     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( '&#10003;' );
     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
    151254    // ── list settings (name source + email) ──────────────────────────────────
    152255    $( '#gu-ml-save' ).on( 'click', function () {
  • goultra-for-woocommerce/trunk/goultra-for-woocommerce.php

    r3601733 r3617733  
    11<?php
    22/**
    3  * Plugin Name:       GoUltra for WooCommerce
     3 * Plugin Name:       GoUltra for WooCommerce - WhatsApp Marketing, Notifications & Abandoned Cart
    44 * 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.1
     5 * Description:       WooCommerce WhatsApp notifications: automated order, shipping and abandoned-cart messages, plus opt-in marketing campaigns, powered by GoUltra.
     6 * Version:           3.9.24
    77 * Requires at least: 6.0
    88 * Requires PHP:      7.4
    … …  
    2525
    2626// Plugin constants
    27 define( 'GOULTRA_WC_VERSION', '3.8.1' );
     27define( 'GOULTRA_WC_VERSION', '3.9.24' );
    2828define( 'GOULTRA_WC_PLUGIN_FILE', __FILE__ );
    2929define( 'GOULTRA_WC_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
    … …  
    167167    require_once GOULTRA_WC_PLUGIN_DIR . 'includes/class-goultra-privacy.php';
    168168    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';
    169172
    170173    // DB migrations
    … …  
    220223    GoUltra_Privacy::instance(); // GDPR export/erase + privacy policy (always on)
    221224    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
    222228}
    223229add_action( 'plugins_loaded', 'goultra_wc_init' );
  • goultra-for-woocommerce/trunk/includes/class-goultra-abandoned-cart.php

    r3599583 r3617733  
    115115        if ( get_transient( 'goultra_sched_ac' ) ) return;
    116116
     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.
    117121        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 );
    119123        }
    120124        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 );
    122126        }
    123127
    … …  
    131135    public function scan_and_schedule() {
    132136        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;
    134138
    135139        global $wpdb;
    … …  
    157161
    158162        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            }
    159177            $this->schedule_sequence( $cart );
    160178        }
    … …  
    218236        );
    219237
    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.
    229248    }
    230249
    … …  
    366385        $result = $api->send_abandoned_cart( $payload );
    367386
    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 ) ) {
    369395            $wpdb->update(
    370396                $table,
    … …  
    375401            $this->log_analytics( 'message_sent', $cart_id, 0, 0 );
    376402        }
     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;
    377431    }
    378432
    … …  
    478532        $table = $wpdb->prefix . 'goultra_abandoned_carts';
    479533
    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
    482556        $where  = array();
    483557        $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 );
    491562        }
    492563        if ( ! empty( $email ) ) {
    … …  
    537608        }
    538609
     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
    539615        // Check if order used a GU- coupon → track revenue
    540616        $coupons = $order->get_coupon_codes();
    … …  
    553629                );
    554630                $this->log_analytics( 'cart_recovered', $cart_id, $order->get_id(), $revenue );
     631                $recovery_logged = true;
    555632                break;
    556633            }
    557634        }
    558635
    559         // Also mark recovered via restore_token click (tracked in session/cookie)
     636        // Also mark recovered via restore_token click (tracked in session/cookie).
    560637        $token = WC()->session ? WC()->session->get( 'goultra_restore_token' ) : '';
    561638        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                }
    569651            }
    570652            WC()->session->set( 'goultra_restore_token', '' );
  • goultra-for-woocommerce/trunk/includes/class-goultra-activator.php

    r3588005 r3617733  
    119119            'connected_phone'          => '',
    120120            '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',
    125125            'abandoned_cart_timeout'   => 15,
    126126            // checkout_optin_text is intentionally NOT seeded here: calling __() during
  • goultra-for-woocommerce/trunk/includes/class-goultra-api.php

    r3599583 r3617733  
    1717    /** @var GoUltra_API|null */
    1818    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;
    1927
    2028    /** @var string */
    … …  
    137145
    138146        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' );
    139150            return array(
    140151                'success'               => true,
    … …  
    143154                'whatsapp_display_name' => $body['whatsapp_display_name'] ?? '',
    144155                'plan'                  => $body['plan']                  ?? 'free',
     156                'profile_picture_url'   => $body['profile_picture_url']   ?? '',
    145157            );
    146158        }
    … …  
    171183    public static function friendly_error( $code ) {
    172184        $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' );
    175219        }
    176220        return $code;
    … …  
    285329
    286330        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 );
    290335        $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        }
    291356        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 );
    292408    }
    293409
    … …  
    306422     * @return array { success: bool, code?: int, response?: array, error?: string }
    307423     */
     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
    308455    public function push_consent( $contacts ) {
    309456        if ( empty( $contacts ) || ! is_array( $contacts ) ) {
    … …  
    450597    }
    451598
     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
    452735    /**
    453736     * Generic event sender.
    … …  
    459742     */
    460743    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;
    463760        $count    = (int) get_transient( $rate_key );
    464         if ( $count >= 60 ) {
     761        if ( $count >= self::RATE_PER_MIN ) {
    465762            $this->log_webhook( $event ?? 'rate_limited', $endpoint, $payload, 429, 'Rate limited' );
    466763            return array( 'success' => false, 'error' => 'Rate limited' );
    467764        }
    468         set_transient( $rate_key, $count + 1, MINUTE_IN_SECONDS );
     765        set_transient( $rate_key, $count + 1, 2 * MINUTE_IN_SECONDS );
    469766
    470767        $body = array(
    … …  
    493790
    494791        $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() );
    495795        return array(
    496796            'success'  => ( $resp_code >= 200 && $resp_code < 300 ),
    … …  
    498798            'response' => $decoded,
    499799        );
     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        }
    500841    }
    501842
    … …  
    546887        $table = $wpdb->prefix . 'goultra_webhook_logs';
    547888
    548         // Sanitize sensitive data from logs
     889        // Sanitize sensitive data from logs.
    549890        $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        }
    550898        if ( isset( $sanitized_body['payload']['customer_phone'] ) ) {
    551899            $phone = $sanitized_body['payload']['customer_phone'];
  • goultra-for-woocommerce/trunk/includes/class-goultra-checkout.php

    r3588005 r3617733  
    190190        }
    191191
    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' ) ) {
    193193            return;
    194194        }
    … …  
    328328            $wpdb->update( $table, $data, array( 'id' => $existing->id ) );
    329329        } 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            }
    330346            $wpdb->insert( $table, $data );
    331347        }
  • goultra-for-woocommerce/trunk/includes/class-goultra-orders.php

    r3599583 r3617733  
    88 * ⚡ PERFORMANCE: every outbound HTTP call to GoUltra is dispatched to an
    99 * Action Scheduler ASYNC job (`goultra_async_send`) instead of running inline on
    10  * the checkout / order-status request. This guarantees the customer's checkout is
     10 * the checkout / order-status request. This ensures the customer's checkout is
    1111 * NEVER blocked or slowed by the GoUltra API (even if it is slow or down).
    1212 * Shipping updates are handled separately by class-goultra-shipping.php.
    … …  
    101101        }
    102102        // 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' ) ) {
    104104            if ( ! $order->get_meta( '_goultra_admin_notify_sent' ) ) {
    105105                $order->update_meta_data( '_goultra_admin_notify_sent', gmdate( 'Y-m-d H:i:s' ) );
    … …  
    116116            // Backward compat: trigger by legacy order_confirm_trigger + enable_order_confirm
    117117            $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' ) ) {
    119119                $should_send_customer = true;
    120120            }
  • goultra-for-woocommerce/trunk/includes/class-goultra-rest-api.php

    r3588005 r3617733  
    125125        if ( empty( $status ) ) {
    126126            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 );
    127140        }
    128141
    … …  
    153166        }
    154167
    155         // ── Log to webhook_logs table ─────────────────────────────────────────
     168        // ── Log to webhook_logs table (phone masked, table bounded) ───────────
    156169        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        }
    157178        $wpdb->insert(
    158             $wpdb->prefix . 'goultra_webhook_logs',
     179            $log_table,
    159180            array(
    160181                'event_type'    => 'delivery_status_' . $status,
    161182                'endpoint'      => 'goultra/v1/webhook',
    162                 'request_body'  => wp_json_encode( $body ),
     183                'request_body'  => wp_json_encode( $masked_body ),
    163184                'response_code' => 200,
    164185                'response_body' => 'ok',
    165186            )
    166187        );
     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        }
    167197
    168198        // ── Analytics: track read/delivered for abandoned cart messages ───────
  • goultra-for-woocommerce/trunk/includes/class-goultra-settings.php

    r3603449 r3617733  
    2525        add_action( 'admin_menu', array( $this, 'add_admin_menu' ) );
    2626        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' ) );
    2827        add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_assets' ) );
    2928        add_action( 'wp_ajax_goultra_verify_api_key', array( $this, 'ajax_verify_api_key' ) );
    … …  
    3433        // Show setup wizard on first install
    3534        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>';
    3663    }
    3764
    … …  
    94121        );
    95122
     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
    96133        // Logs submenu
    97134        add_submenu_page(
    … …  
    115152            'goultra_page_goultra-templates',
    116153            'goultra_page_goultra-subscribers',
     154            'goultra_page_goultra-campaigns',
    117155            'goultra_page_goultra-logs',
    118156        );
    … …  
    173211                    'none'          => __( 'No subscribers yet. They appear here once shoppers opt in to WhatsApp marketing at checkout.', 'goultra-for-woocommerce' ),
    174212                    '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' ),
    175220                    'error'         => __( 'Could not load subscribers. Please try again.', 'goultra-for-woocommerce' ),
    176221                    'subscribed'    => __( 'Subscribed', 'goultra-for-woocommerce' ),
    … …  
    191236            ) );
    192237        }
     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        }
    193301    }
    194302
    … …  
    217325            update_option( GOULTRA_WC_OPTION_PREFIX . 'business_name', $result['business_name'] ?? '' );
    218326            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 );
    219335            $api->refresh_api_key();
    220336
    … …  
    233349            }
    234350
     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
    235361            wp_send_json_success( $result );
    236362        } else {
    237363            wp_send_json_error( $result['error'] );
    238364        }
     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;
    239408    }
    240409
    … …  
    284453            'ac_discount_type'         => 'sanitize_text_field',
    285454            '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',
    286463        );
    287464
    … …  
    330507        }
    331508
     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
    332521        update_option( GOULTRA_WC_OPTION_PREFIX . 'api_key', '' );
    333522        update_option( GOULTRA_WC_OPTION_PREFIX . 'connected', false );
    … …  
    352541        delete_transient( 'goultra_wc_remote_templates' );
    353542
     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
    354557        wp_send_json_success();
    355558    }
    … …  
    414617
    415618    /**
     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    /**
    416627     * AJAX: Debug sync - returns raw data from GoUltra and updates WP DB.
    417628     */
    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 phone
    433         $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 IL
    440         }
    441 
    442         if ( $order_id > 0 ) {
    443             $order = wc_get_order( $order_id );
    444             if ( $order ) {
    445                 // Clear dedup so it can send again
    446                 $order->delete_meta_data( '_goultra_order_confirm_sent' );
    447                 $order->save();
    448                 // Force send
    449                 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 call
    461         $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 
    478629    public function ajax_debug_sync() {
    479630        check_ajax_referer( 'goultra_admin_nonce', 'nonce' );
  • goultra-for-woocommerce/trunk/includes/class-goultra-shipping.php

    r3599583 r3617733  
    4444        }
    4545
    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' ) ) {
    4747            return;
    4848        }
  • goultra-for-woocommerce/trunk/includes/class-goultra-subscribers.php

    r3603449 r3617733  
    5252        add_action( 'wp_ajax_goultra_backfill_subscribers',    array( $this, 'ajax_backfill' ) );
    5353        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' ) );
    5456
    5557        if ( ! get_option( GOULTRA_WC_OPTION_PREFIX . 'connected', false ) ) {
    … …  
    132134        update_option( GOULTRA_WC_OPTION_PREFIX . 'ml_include_email', ( '1' === ( $_POST['include_email'] ?? '' ) ) ? 'yes' : 'no' );
    133135        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 ) );
    134205    }
    135206
  • goultra-for-woocommerce/trunk/includes/class-goultra-templates.php

    r3588005 r3617733  
    122122        'abandoned_cart_2'   => 'Abandoned Cart - Step 2 (Urgency)',
    123123        'abandoned_cart_3'   => 'Abandoned Cart - Step 3 (Discount)',
     124        'marketing_offer'    => 'Marketing Offer',
    124125        'custom'             => 'Custom Template',
    125126    );
    … …  
    140141        add_action( 'wp_ajax_goultra_fetch_remote_templates', array( $this, 'ajax_fetch_remote_templates' ) );
    141142        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' );
    142376    }
    143377
    … …  
    430664        // broke the saved per-status template selections (toggle stayed on, template dropped).
    431665
    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%' )";
    433667
    434668        // Existing woo_* rows: template_name => id. Unnamed or duplicate-name rows are queued
    … …  
    444678
    445679        $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' );
    447681
    448682        $seen_names = array();
    … …  
    469703            // Extract style_key from template_name if not provided by API
    470704            // 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.
    471708            $style_key = $r['style_key'] ?? '';
    472709            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                );
    474714                $prefix = 'woo_' . $tpl_key . '_';
    475715                if ( strpos( $name, $prefix ) === 0 ) {
    476716                    $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                        }
    480722                    }
    481723                }
  • goultra-for-woocommerce/trunk/languages/goultra-for-woocommerce-ar.po

    r3603449 r3617733  
    925925msgid "Send a test message (optional)"
    926926msgstr "إرسال رسالة تجريبية (اختياري)"
     927
     928msgid "Campaigns"
     929msgstr "الحملات"
     930
     931msgid "New campaign"
     932msgstr "حملة جديدة"
     933
     934msgid "Compose a WhatsApp campaign for your Marketing List subscribers. Unsubscribed contacts are always filtered out automatically."
     935msgstr "أنشئ حملة واتساب لمشتركي قائمتك التسويقية. تتم تصفية جهات الاتصال الملغاة تلقائياً دائماً."
     936
     937msgid "Connect your store to GoUltra to send campaigns."
     938msgstr "اربط متجرك بـ GoUltra لإرسال الحملات."
     939
     940msgid "Template"
     941msgstr "القالب"
     942
     943msgid "Refresh"
     944msgstr "تحديث"
     945
     946msgid "No approved marketing templates yet. Create one below."
     947msgstr "لا توجد قوالب تسويقية معتمدة بعد. أنشئ واحداً أدناه."
     948
     949msgid "Variables"
     950msgstr "المتغيرات"
     951
     952msgid "Header image"
     953msgstr "صورة الترويسة"
     954
     955msgid "Media Library"
     956msgstr "مكتبة الوسائط"
     957
     958msgid "Pick a product"
     959msgstr "اختيار منتج"
     960
     961msgid "Search products"
     962msgstr "البحث في المنتجات"
     963
     964msgid "Send a test to your phone first"
     965msgstr "أرسل تجربة إلى هاتفك أولاً"
     966
     967msgid "Send test"
     968msgstr "إرسال تجربة"
     969
     970msgid "Test sent. Check your WhatsApp."
     971msgstr "تم إرسال التجربة. تحقق من واتساب لديك."
     972
     973msgid "Audience"
     974msgstr "الجمهور"
     975
     976msgid "When to send"
     977msgstr "موعد الإرسال"
     978
     979msgid "Send now"
     980msgstr "إرسال الآن"
     981
     982msgid "Schedule for later"
     983msgstr "جدولة لوقت لاحق"
     984
     985msgid "Launch campaign"
     986msgstr "إطلاق الحملة"
     987
     988msgid "online"
     989msgstr "متصل"
     990
     991msgid "Choose a template to see the preview."
     992msgstr "اختر قالباً لرؤية المعاينة."
     993
     994msgid "Campaign history"
     995msgstr "سجل الحملات"
     996
     997msgid "Sent"
     998msgstr "أُرسلت"
     999
     1000msgid "Failed"
     1001msgstr "فشلت"
     1002
     1003msgid "Create a marketing template"
     1004msgstr "إنشاء قالب تسويقي"
     1005
     1006msgid "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."
     1007msgstr "اختر لغةً ونمطاً، عدّل النص وأرسله لموافقة Meta. تُضاف سطر إلغاء الاشتراك بلغة القالب تلقائياً كما تشترط Meta للتسويق."
     1008
     1009msgid "Language"
     1010msgstr "اللغة"
     1011
     1012msgid "Style"
     1013msgstr "النمط"
     1014
     1015msgid "Message text"
     1016msgstr "نص الرسالة"
     1017
     1018msgid "Button label (optional)"
     1019msgstr "نص الزر (اختياري)"
     1020
     1021msgid "Button link (optional)"
     1022msgstr "رابط الزر (اختياري)"
     1023
     1024msgid "Submit for approval"
     1025msgstr "إرسال للموافقة"
     1026
     1027msgid "Something went wrong. Please try again."
     1028msgstr "حدث خطأ ما. حاول مرة أخرى."
     1029
     1030msgid "Choose a template"
     1031msgstr "اختر قالباً"
     1032
     1033msgid "Choose a template first."
     1034msgstr "اختر قالباً أولاً."
     1035
     1036msgid "Awaiting Meta approval:"
     1037msgstr "بانتظار موافقة Meta:"
     1038
     1039msgid "Customer first name"
     1040msgstr "الاسم الأول للعميل"
     1041
     1042msgid "Customer full name"
     1043msgstr "الاسم الكامل للعميل"
     1044
     1045msgid "Fixed value"
     1046msgstr "قيمة ثابتة"
     1047
     1048msgid "No products with a usable image were found."
     1049msgstr "لم يُعثر على منتجات بصورة مناسبة."
     1050
     1051msgid "Sending..."
     1052msgstr "جارٍ الإرسال..."
     1053
     1054msgid "Please fill in all template variables."
     1055msgstr "يرجى تعبئة جميع متغيرات القالب."
     1056
     1057msgid "This template needs a header image. Choose one from the media library, a product, or paste an https image link."
     1058msgstr "يحتاج هذا القالب إلى صورة ترويسة. اختر من مكتبة الوسائط أو من منتج أو الصق رابط https للصورة."
     1059
     1060msgid "%d subscribers will receive this campaign."
     1061msgstr "سيتلقى هذه الحملة %d مشتركاً."
     1062
     1063msgid "Send this campaign to %d subscribers now?"
     1064msgstr "هل تريد إرسال هذه الحملة إلى %d مشتركاً الآن؟"
     1065
     1066msgid "Schedule this campaign to %d subscribers?"
     1067msgstr "هل تريد جدولة هذه الحملة إلى %d مشتركاً؟"
     1068
     1069msgid "Campaign launched. Messages are on their way."
     1070msgstr "انطلقت الحملة. الرسائل في طريقها."
     1071
     1072msgid "Campaign scheduled."
     1073msgstr "تمت جدولة الحملة."
     1074
     1075msgid "No campaigns yet."
     1076msgstr "لا توجد حملات بعد."
     1077
     1078msgid "Pause"
     1079msgstr "إيقاف مؤقت"
     1080
     1081msgid "Cancel"
     1082msgstr "إلغاء"
     1083
     1084msgid "Pause this campaign?"
     1085msgstr "هل تريد إيقاف الحملة مؤقتاً؟"
     1086
     1087msgid "Cancel this scheduled campaign?"
     1088msgstr "هل تريد إلغاء الحملة المجدولة؟"
     1089
     1090msgid "Processing"
     1091msgstr "قيد المعالجة"
     1092
     1093msgid "Scheduled"
     1094msgstr "مجدولة"
     1095
     1096msgid "Completed"
     1097msgstr "اكتملت"
     1098
     1099msgid "Cancelled"
     1100msgstr "أُلغيت"
     1101
     1102msgid "Paused"
     1103msgstr "موقوفة مؤقتاً"
     1104
     1105msgid "Opt-out footer (added automatically):"
     1106msgstr "سطر إلغاء الاشتراك (يُضاف تلقائياً):"
     1107
     1108msgid "The message text must be between 15 and 1024 characters."
     1109msgstr "يجب أن يكون نص الرسالة بين 15 و1024 حرفاً."
     1110
     1111msgid "Template submitted. Meta approval usually takes minutes to a few hours."
     1112msgstr "تم إرسال القالب. موافقة Meta تستغرق عادة من دقائق إلى بضع ساعات."
     1113
     1114msgid "A button needs both a label and a link."
     1115msgstr "يحتاج الزر إلى نص ورابط معاً."
     1116
     1117msgid "Could not submit the template. Please try again."
     1118msgstr "تعذّر إرسال القالب. حاول مرة أخرى."
     1119
     1120msgid "Could not load templates. Please try again."
     1121msgstr "تعذّر تحميل القوالب. حاول مرة أخرى."
     1122
     1123msgid "Test send failed. Please try again."
     1124msgstr "فشل إرسال التجربة. حاول مرة أخرى."
     1125
     1126msgid "Could not launch the campaign. Please try again."
     1127msgstr "تعذّر إطلاق الحملة. حاول مرة أخرى."
     1128
     1129msgid "The action failed. Please try again."
     1130msgstr "فشلت العملية. حاول مرة أخرى."
     1131
     1132msgid "Invalid request."
     1133msgstr "طلب غير صالح."
     1134
     1135msgid "Choose a valid date and time for the scheduled send."
     1136msgstr "اختر تاريخاً ووقتاً صالحين للإرسال المجدول."
     1137
     1138msgid "A campaign was just launched. Please wait two minutes before launching another."
     1139msgstr "أُطلقت حملة للتو. انتظر دقيقتين قبل إطلاق أخرى."
     1140
     1141msgid "Your audience is larger than 5,000 contacts. Please run this campaign from the GoUltra dashboard."
     1142msgstr "جمهورك أكبر من 5,000 جهة اتصال. شغّل هذه الحملة من لوحة تحكم GoUltra."
     1143
     1144msgid "Your GoUltra account has more than one WhatsApp number connected, so the sending number must be chosen in the GoUltra dashboard for this store."
     1145msgstr "حسابك في GoUltra موصول بأكثر من رقم واتساب، لذا يجب اختيار رقم الإرسال لهذا المتجر من لوحة تحكم GoUltra."
     1146
     1147msgid "Your GoUltra plan message allowance for this cycle has been reached. Upgrade your plan to send more."
     1148msgstr "استُهلكت حصة الرسائل في خطة GoUltra لهذه الدورة. رقِّ خطتك لإرسال المزيد."
     1149
     1150msgid "GoUltra for WooCommerce - WhatsApp Marketing, Notifications & Abandoned Cart"
     1151msgstr "GoUltra لـ WooCommerce - تسويق وإشعارات وسلة متروكة عبر WhatsApp"
     1152
     1153msgid "WooCommerce WhatsApp notifications: automated order, shipping and abandoned-cart messages, plus opt-in marketing campaigns, powered by GoUltra."
     1154msgstr "إشعارات WhatsApp لـ WooCommerce: رسائل تلقائية لتأكيد الطلب والشحن والسلة المتروكة، بالإضافة إلى حملات تسويقية بموافقة، بواسطة GoUltra."
     1155
     1156msgid "Marketing Offer"
     1157msgstr "عرض تسويقي"
     1158
     1159msgid "Promotions and offers for opted-in subscribers"
     1160msgstr "عروض وتخفيضات للمشتركين الموافقين"
     1161
     1162msgid "Recipients"
     1163msgstr "المستلمون"
     1164
     1165msgid "All subscribers"
     1166msgstr "جميع المشتركين"
     1167
     1168msgid "Choose specific contacts"
     1169msgstr "اختيار جهات اتصال محددة"
     1170
     1171msgid "Search name or number"
     1172msgstr "ابحث بالاسم أو الرقم"
     1173
     1174msgid "Select all"
     1175msgstr "تحديد الكل"
     1176
     1177msgid "Clear"
     1178msgstr "مسح"
     1179
     1180msgid "Also send to my own number (%s) for a test"
     1181msgstr "أرسل أيضاً إلى رقمي (%s) للاختبار"
     1182
     1183msgid "Also send to my own number for a test (set your admin number in Settings)"
     1184msgstr "أرسل أيضاً إلى رقمي للاختبار (عيّن رقم المدير في الإعدادات)"
     1185
     1186msgid "Need a marketing template?"
     1187msgstr "تحتاج قالباً تسويقياً؟"
     1188
     1189msgid "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."
     1190msgstr "أنشئه من تبويب القوالب (عرض تسويقي): اختر لغة ونمطاً جاهزاً، وتُضاف سطر إلغاء الاشتراك تلقائياً. تظهر القوالب المعتمدة في المُنتقي أعلاه."
     1191
     1192msgid "Open Templates"
     1193msgstr "فتح القوالب"
     1194
     1195msgid "Delivery receipts and product sync"
     1196msgstr "إيصالات التسليم ومزامنة المنتجات"
     1197
     1198msgid "These work automatically once your store is connected. There is nothing to set up."
     1199msgstr "تعمل هذه تلقائياً بمجرد ربط متجرك. لا يوجد ما يتطلب الإعداد."
     1200
     1201msgid "Connected and active."
     1202msgstr "متصل ونشط."
     1203
     1204msgid "Message delivery receipts appear as order notes, and your product catalog is available to GoUltra automatically. No secret or URL to copy anywhere."
     1205msgstr "تظهر إيصالات تسليم الرسائل كملاحظات على الطلب، ويكون كتالوج منتجاتك متاحاً لـ GoUltra تلقائياً. لا يوجد سر أو رابط لنسخه."
     1206
     1207msgid "Technical details (for support)"
     1208msgstr "تفاصيل تقنية (للدعم)"
     1209
     1210msgid "Webhook secret"
     1211msgstr "سر Webhook"
     1212
     1213msgid "The plugin shares the secret with GoUltra automatically on connect, so you never paste it anywhere."
     1214msgstr "يشارك الإضافة السر مع GoUltra تلقائياً عند الاتصال، لذا لا تحتاج إلى لصقه في أي مكان."
     1215
     1216msgid "All subscribers (%d)"
     1217msgstr "جميع المشتركين (%d)"
     1218
     1219msgid "%d selected"
     1220msgstr "%d محدد"
     1221
     1222msgid "No subscribers yet."
     1223msgstr "لا يوجد مشتركون بعد."
     1224
     1225msgid "No recipients selected. Choose subscribers, or add your own number to send yourself a test."
     1226msgstr "لم يتم اختيار مستلمين. اختر مشتركين، أو أضف رقمك لإرسال اختبار لنفسك."
     1227
     1228msgid "%d recipients will receive this campaign."
     1229msgstr "سيتلقى هذه الحملة %d مستلماً."
     1230
     1231msgid "Send this campaign to %d recipients now?"
     1232msgstr "هل تريد إرسال هذه الحملة إلى %d مستلماً الآن؟"
     1233
     1234msgid "Schedule this campaign to %d recipients?"
     1235msgstr "هل تريد جدولة هذه الحملة إلى %d مستلماً؟"
     1236
     1237msgid "Add my number"
     1238msgstr "إضافة رقمي"
     1239
     1240msgid "Add to the list"
     1241msgstr "إضافة إلى القائمة"
     1242
     1243msgid "Adding..."
     1244msgstr "جارٍ الإضافة..."
     1245
     1246msgid "Added to the list."
     1247msgstr "تمت الإضافة إلى القائمة."
     1248
     1249msgid "Could not add the contact. Please try again."
     1250msgstr "تعذّرت إضافة جهة الاتصال. حاول مرة أخرى."
     1251
     1252msgid "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."
     1253msgstr "يضيف الرقم إلى قائمة التسويق كجهة اتصال مشترِكة. مفيد لإرسال حملة تجريبية لنفسك: في شاشة الحملات اختر \"اختيار جهات اتصال محددة\" وحدد رقمك فقط."
     1254
     1255msgid "Tip: to test on yourself, add your own number on the Marketing List screen, then select only it here."
     1256msgstr "نصيحة: للاختبار على نفسك، أضف رقمك في شاشة قائمة التسويق ثم حدده هنا فقط."
     1257
     1258msgid "No recipients selected. Choose contacts from the list."
     1259msgstr "لم يتم اختيار مستلمين. اختر جهات اتصال من القائمة."
     1260
     1261msgid "No name"
     1262msgstr "بدون اسم"
     1263
     1264msgid "Today"
     1265msgstr "اليوم"
     1266
     1267msgid "Your GoUltra account is currently on hold, so messages cannot be sent. Please contact GoUltra support to resume."
     1268msgstr "حساب GoUltra الخاص بك موقوف حالياً، لذا لا يمكن إرسال الرسائل. يرجى التواصل مع دعم GoUltra للاستئناف."
     1269
     1270msgid "Edit name"
     1271msgstr "تعديل الاسم"
     1272
     1273msgid "Schedule campaign"
     1274msgstr "جدولة الحملة"
     1275
     1276msgid "Will be sent on %1$s at %2$s (your store time)."
     1277msgstr "سيُرسَل في %1$s الساعة %2$s (بتوقيت متجرك)."
     1278
     1279msgid "Could not update the name. Please try again."
     1280msgstr "تعذّر تحديث الاسم. حاول مرة أخرى."
     1281
     1282msgid "GoUltra returned HTTP %d."
     1283msgstr "أعادت GoUltra رمز HTTP %d."
     1284
     1285msgid "Template name"
     1286msgstr "اسم القالب"
     1287
     1288msgid "Lowercase letters, numbers and underscores only."
     1289msgstr "أحرف إنجليزية صغيرة وأرقام وشرطة سفلية فقط."
     1290
     1291msgid "Marketing (offers, promotions)"
     1292msgstr "تسويقي (عروض، تخفيضات)"
     1293
     1294msgid "Utility (order, account updates)"
     1295msgstr "خدمي (طلب، تحديثات الحساب)"
     1296
     1297msgid "No header"
     1298msgstr "بدون ترويسة"
     1299
     1300msgid "Text"
     1301msgstr "نص"
     1302
     1303msgid "Video"
     1304msgstr "فيديو"
     1305
     1306msgid "Document"
     1307msgstr "مستند"
     1308
     1309msgid "Header text"
     1310msgstr "نص الترويسة"
     1311
     1312msgid "You will choose the actual image/video/file when you send a campaign."
     1313msgstr "ستختار الصورة/الفيديو/الملف الفعلي عند إرسال حملة."
     1314
     1315msgid "Note:"
     1316msgstr "ملاحظة:"
     1317
     1318msgid "Utility templates are for transactional messages only (order, shipping, account). Promotional content will be rejected by Meta."
     1319msgstr "القوالب الخدمية مخصصة للرسائل المعاملاتية فقط (طلب، شحن، حساب). سيرفض Meta المحتوى الترويجي."
     1320
     1321msgid "Insert a variable"
     1322msgstr "إدراج متغير"
     1323
     1324msgid "(filled in automatically at send time)"
     1325msgstr "(يُملأ تلقائياً عند الإرسال)"
     1326
     1327msgid "Buttons"
     1328msgstr "الأزرار"
     1329
     1330msgid "(up to 10, optional)"
     1331msgstr "(حتى 10، اختياري)"
     1332
     1333msgid "Add button"
     1334msgstr "إضافة زر"
     1335
     1336msgid "Opt-out method (required for marketing)"
     1337msgstr "طريقة إلغاء الاشتراك (إلزامية للتسويق)"
     1338
     1339msgid "Footer text (recommended)"
     1340msgstr "نص التذييل (موصى به)"
     1341
     1342msgid "Unsubscribe button"
     1343msgstr "زر إلغاء الاشتراك"
     1344
     1345msgid "Footer"
     1346msgstr "التذييل"
     1347
     1348msgid "(small gray line, optional)"
     1349msgstr "(سطر رمادي صغير، اختياري)"
     1350
     1351msgid "Quick reply"
     1352msgstr "رد سريع"
     1353
     1354msgid "URL link"
     1355msgstr "رابط URL"
     1356
     1357msgid "Phone number"
     1358msgstr "رقم الهاتف"
     1359
     1360msgid "Button text"
     1361msgstr "نص الزر"
     1362
     1363msgid "A URL or phone button needs a value."
     1364msgstr "زر URL أو الهاتف يحتاج إلى قيمة."
     1365
     1366msgid "Remove"
     1367msgstr "إزالة"
     1368
     1369msgid "Two variables cannot be next to each other. Add some text between them (Meta rejects it otherwise)."
     1370msgstr "لا يمكن وضع متغيرين متجاورين. أضف نصاً بينهما (وإلا سيرفضه Meta)."
     1371
     1372msgid "The message text must be between 5 and 1024 characters."
     1373msgstr "يجب أن يكون نص الرسالة بين 5 و1024 حرفاً."
     1374
     1375msgid "Meta rejected the template. %1$s (fallback: %2$s)"
     1376msgstr "رفض Meta القالب. %1$s (بديل: %2$s)"
     1377
     1378msgid "Unknown error."
     1379msgstr "خطأ غير معروف."
     1380
     1381msgid "The template could not be submitted. %s"
     1382msgstr "تعذر إرسال القالب. %s"
     1383
     1384msgid "This number is not on your marketing list, so there is no name to update."
     1385msgstr "هذا الرقم غير موجود في قائمتك التسويقية، لذا لا يوجد اسم لتحديثه."
     1386
     1387msgid "Paste your API key (it starts with gk_) from the GoUltra Dashboard → Settings → API."
     1388msgstr "الصق مفتاح API (يبدأ بـ gk_) من لوحة GoUltra → الإعدادات → API."
     1389
     1390msgid "Campaign name (optional)"
     1391msgstr "اسم الحملة (اختياري)"
     1392
     1393msgid "e.g. Summer Sale - July"
     1394msgstr "مثال: تخفيضات الصيف - يوليو"
     1395
     1396msgid "A name to recognize this campaign in your history."
     1397msgstr "اسم يساعدك على تمييز هذه الحملة في السجل."
     1398
     1399msgid "Upload / Media Library"
     1400msgstr "رفع / مكتبة الوسائط"
     1401
     1402msgid "Image selected"
     1403msgstr "تم اختيار الصورة"
     1404
     1405msgid "Or paste an image URL"
     1406msgstr "أو الصق رابط صورة"
     1407
     1408msgid "JPEG or PNG images only. Choose from your Media Library, pick a store product, or paste a URL."
     1409msgstr "صور JPEG أو PNG فقط. اختر من مكتبة الوسائط، أو اختر منتجاً من المتجر، أو الصق رابطاً."
     1410
     1411msgid "Enter a valid WhatsApp number, including the country code."
     1412msgstr "أدخل رقم WhatsApp صالحاً، بما في ذلك رمز الدولة."
     1413
     1414msgid "Too many code requests. Please wait a while before trying again."
     1415msgstr "طلبات كود كثيرة جداً. يرجى الانتظار قليلاً ثم المحاولة مجدداً."
     1416
     1417msgid "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."
     1418msgstr "لا يزال قالب التحقق الخاص بك على WhatsApp قيد الموافقة من Meta (عادةً بضع دقائق، وأحياناً أطول). حاول مرة أخرى بعد قليل - لا حاجة لأي إجراء إضافي منك."
     1419
     1420msgid "Could not send the code. Please check the number and try again."
     1421msgstr "تعذر إرسال الكود. تحقق من الرقم وحاول مجدداً."
     1422
     1423msgid "We sent a 6-digit code to %s on WhatsApp. Enter it below."
     1424msgstr "أرسلنا رمزاً من 6 أرقام إلى %s على WhatsApp. أدخله أدناه."
     1425
     1426msgid "The code expired. Please request a new one."
     1427msgstr "انتهت صلاحية الكود. اطلب رمزاً جديداً."
     1428
     1429msgid "Too many wrong attempts. Please request a new code."
     1430msgstr "محاولات خاطئة كثيرة جداً. اطلب رمزاً جديداً."
     1431
     1432msgid "Incorrect code. Attempts left: %d."
     1433msgstr "رمز غير صحيح. المحاولات المتبقية: %d."
     1434
     1435msgid "Your number is verified. It is now your store manager number and can be selected as a campaign recipient for your own tests."
     1436msgstr "تم التحقق من رقمك. أصبح الآن رقم مدير المتجر، ويمكن اختياره كمستلم في الحملة لاختباراتك."
     1437
     1438msgid "Store manager"
     1439msgstr "مدير المتجر"
     1440
     1441msgid "Verify your number (recommended)"
     1442msgstr "تحقق من رقمك (موصى به)"
     1443
     1444msgid "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."
     1445msgstr "أكّد رقم WhatsApp الخاص بك برمز. يصبح الرقم الموثّق رقم مدير المتجر، ويمكنك اختياره كمستلم في الحملة لاختبار حملاتك."
     1446
     1447msgid "Verified"
     1448msgstr "موثّق"
     1449
     1450msgid "Verify a different number"
     1451msgstr "التحقق من رقم آخر"
     1452
     1453msgid "Send code"
     1454msgstr "إرسال الرمز"
     1455
     1456msgid "Verify"
     1457msgstr "تحقق"
     1458
     1459msgid "Resend code"
     1460msgstr "إعادة إرسال الرمز"
     1461
     1462msgid "Verifying..."
     1463msgstr "جارٍ التحقق..."
     1464
     1465msgid "Enter your WhatsApp number first."
     1466msgstr "أدخل رقم WhatsApp الخاص بك أولاً."
     1467
     1468msgid "Enter the code you received."
     1469msgstr "أدخل الرمز الذي استلمته."
     1470
     1471msgid "Could not send the code."
     1472msgstr "تعذر إرسال الرمز."
     1473
     1474msgid "Incorrect code."
     1475msgstr "رمز غير صحيح."
     1476
     1477msgid "Network error. Please try again."
     1478msgstr "خطأ في الشبكة. حاول مرة أخرى."
     1479
     1480msgid "Building a template for a product?"
     1481msgstr "هل تبني قالباً لمنتج؟"
     1482
     1483msgid "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."
     1484msgstr "اختر منتجاً من المتجر وسنجهّز القالب له: ترويسة صورة وزر \"عرض المنتج\" يرتبط به. ثم اختر لغة وأرسله للموافقة."
     1485
     1486msgid "View product"
     1487msgstr "عرض المنتج"
     1488
     1489msgid "No products found."
     1490msgstr "لم يتم العثور على منتجات."
     1491
     1492msgid "You already have an approved or pending template of this type for this language. Create another one?"
     1493msgstr "لديك بالفعل قالب موافق عليه أو قيد الانتظار من هذا النوع بهذه اللغة. إنشاء واحد آخر؟"
     1494
     1495msgid "Unconfirmed - verify before resending"
     1496msgstr "غير مؤكد - تحقق قبل إعادة الإرسال"
     1497
     1498msgid "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."
     1499msgstr "انقطع الاتصال قبل أن نتلقى ردا، لذا قد يكون تم إرسال الحملة أو لا. تحقق من سجل الحملات ولوحة تحكم GoUltra قبل إطلاقها مرة أخرى لتجنب إرسالها مرتين."
     1500
     1501msgid "GoUltra for WooCommerce: connection problem."
     1502msgstr "GoUltra لـ WooCommerce: مشكلة في الاتصال."
     1503
     1504msgid "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."
     1505msgstr "تم رفض رسائل WhatsApp الأخيرة لأن مفتاح API لم يعد صالحا (ربما تم إنشاؤه من جديد أو تغير الحساب). الرسائل لا يتم تسليمها."
     1506
     1507msgid "Reconnect your store"
     1508msgstr "أعد ربط متجرك"
     1509
     1510msgid "Image"
     1511msgstr "صورة"
     1512
     1513msgid "View"
     1514msgstr "عرض"
     1515
     1516msgid "rejected"
     1517msgstr "مرفوضة"
     1518
     1519msgid "No templates match your search."
     1520msgstr "لا توجد قوالب تطابق بحثك."
     1521
     1522msgid "Search your templates by name, text or language..."
     1523msgstr "ابحث في قوالبك بالاسم أو النص أو اللغة..."
     1524
     1525msgid "Close"
     1526msgstr "إغلاق"
     1527
     1528msgid "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."
     1529msgstr "لم تقبل GoUltra مفتاح هذا المتجر لهذا الإجراء. اتصالك سليم (رسائل الطلبات تُرسل)، لكن قد لا يكون إرسال الحملات لهذا الرقم مُفعّلًا بعد في حسابك على GoUltra. يرجى التواصل مع دعم GoUltra لتفعيله."
     1530
     1531msgid "JPEG or PNG images only."
     1532msgstr "صور JPEG أو PNG فقط."
     1533
     1534msgid "Marketing sending is turned off for this WhatsApp number. Enable marketing for this number in your GoUltra dashboard to send campaigns."
     1535msgstr "إرسال الرسائل التسويقية مُعطّل لرقم WhatsApp هذا. فعّل التسويق لهذا الرقم في لوحة تحكم GoUltra لإرسال الحملات."
     1536
     1537msgid "Chat with us"
     1538msgstr "تحدث معنا"
     1539
     1540msgid "Chat on WhatsApp"
     1541msgstr "الدردشة عبر WhatsApp"
     1542
     1543msgid "about %s"
     1544msgstr "حول %s"
     1545
     1546msgid "Ask about this product on WhatsApp"
     1547msgstr "اسأل عن هذا المنتج عبر WhatsApp"
     1548
     1549msgid "WhatsApp Chat Button"
     1550msgstr "زر الدردشة على WhatsApp"
     1551
     1552msgid "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."
     1553msgstr "أضف إلى متجرك زر دردشة WhatsApp مجاني بنقرة واحدة. يعمل بدون حساب GoUltra: يفتح دردشة WhatsApp إلى رقمك باستخدام رابط دردشة قياسي."
     1554
     1555msgid "Show the floating chat button"
     1556msgstr "إظهار زر الدردشة العائم"
     1557
     1558msgid "A round WhatsApp button in the corner of your storefront."
     1559msgstr "زر WhatsApp دائري في زاوية متجرك."
     1560
     1561msgid "Include the country code. This is the number that will receive the chats."
     1562msgstr "أدرج رمز الدولة. هذا هو الرقم الذي سيستقبل الدردشات."
     1563
     1564msgid "Pre-filled message"
     1565msgstr "رسالة معبأة مسبقاً"
     1566
     1567msgid "Hi! I have a question about your store."
     1568msgstr "مرحباً! لدي سؤال حول متجرك."
     1569
     1570msgid "The message the customer starts with. Leave empty for none."
     1571msgstr "الرسالة التي يبدأ بها العميل. اتركها فارغة لعدم استخدام أي رسالة."
     1572
     1573msgid "Button label"
     1574msgstr "نص الزر"
     1575
     1576msgid "Position"
     1577msgstr "الموضع"
     1578
     1579msgid "Bottom right"
     1580msgstr "أسفل اليمين"
     1581
     1582msgid "Bottom left"
     1583msgstr "أسفل اليسار"
     1584
     1585msgid "Show on"
     1586msgstr "إظهار في"
     1587
     1588msgid "All pages"
     1589msgstr "كل الصفحات"
     1590
     1591msgid "Shop and product pages only"
     1592msgstr "صفحات المتجر والمنتجات فقط"
     1593
     1594msgid "Add a button on product pages"
     1595msgstr "أضف زراً في صفحات المنتجات"
     1596
     1597msgid "Shows an \"Ask about this product\" WhatsApp button under Add to cart, with the product name filled in."
     1598msgstr "يعرض زر WhatsApp \"اسأل عن هذا المنتج\" أسفل زر الإضافة إلى السلة، مع اسم المنتج."
     1599
     1600msgid "Tip: you can also place a button anywhere with the shortcode"
     1601msgstr "نصيحة: يمكنك أيضاً وضع زر في أي مكان باستخدام الشورت كود"
  • goultra-for-woocommerce/trunk/languages/goultra-for-woocommerce-es_ES.po

    r3603449 r3617733  
    913913msgid "Send a test message (optional)"
    914914msgstr "Enviar un mensaje de prueba (opcional)"
     915
     916msgid "Campaigns"
     917msgstr "Campañas"
     918
     919msgid "New campaign"
     920msgstr "Nueva campaña"
     921
     922msgid "Compose a WhatsApp campaign for your Marketing List subscribers. Unsubscribed contacts are always filtered out automatically."
     923msgstr "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
     925msgid "Connect your store to GoUltra to send campaigns."
     926msgstr "Conecta tu tienda a GoUltra para enviar campañas."
     927
     928msgid "Template"
     929msgstr "Plantilla"
     930
     931msgid "Refresh"
     932msgstr "Actualizar"
     933
     934msgid "No approved marketing templates yet. Create one below."
     935msgstr "Aún no hay plantillas de marketing aprobadas. Crea una abajo."
     936
     937msgid "Variables"
     938msgstr "Variables"
     939
     940msgid "Header image"
     941msgstr "Imagen de cabecera"
     942
     943msgid "Media Library"
     944msgstr "Biblioteca de medios"
     945
     946msgid "Pick a product"
     947msgstr "Elegir un producto"
     948
     949msgid "Search products"
     950msgstr "Buscar productos"
     951
     952msgid "Send a test to your phone first"
     953msgstr "Envía primero una prueba a tu teléfono"
     954
     955msgid "Send test"
     956msgstr "Enviar prueba"
     957
     958msgid "Test sent. Check your WhatsApp."
     959msgstr "Prueba enviada. Revisa tu WhatsApp."
     960
     961msgid "Audience"
     962msgstr "Audiencia"
     963
     964msgid "When to send"
     965msgstr "Cuándo enviar"
     966
     967msgid "Send now"
     968msgstr "Enviar ahora"
     969
     970msgid "Schedule for later"
     971msgstr "Programar para más tarde"
     972
     973msgid "Launch campaign"
     974msgstr "Lanzar la campaña"
     975
     976msgid "online"
     977msgstr "en línea"
     978
     979msgid "Choose a template to see the preview."
     980msgstr "Elige una plantilla para ver la vista previa."
     981
     982msgid "Campaign history"
     983msgstr "Historial de campañas"
     984
     985msgid "Sent"
     986msgstr "Enviados"
     987
     988msgid "Failed"
     989msgstr "Fallidos"
     990
     991msgid "Create a marketing template"
     992msgstr "Crear una plantilla de marketing"
     993
     994msgid "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."
     995msgstr "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
     997msgid "Language"
     998msgstr "Idioma"
     999
     1000msgid "Style"
     1001msgstr "Estilo"
     1002
     1003msgid "Message text"
     1004msgstr "Texto del mensaje"
     1005
     1006msgid "Button label (optional)"
     1007msgstr "Etiqueta del botón (opcional)"
     1008
     1009msgid "Button link (optional)"
     1010msgstr "Enlace del botón (opcional)"
     1011
     1012msgid "Submit for approval"
     1013msgstr "Enviar para aprobación"
     1014
     1015msgid "Something went wrong. Please try again."
     1016msgstr "Algo salió mal. Inténtalo de nuevo."
     1017
     1018msgid "Choose a template"
     1019msgstr "Elige una plantilla"
     1020
     1021msgid "Choose a template first."
     1022msgstr "Elige primero una plantilla."
     1023
     1024msgid "Awaiting Meta approval:"
     1025msgstr "Esperando la aprobación de Meta:"
     1026
     1027msgid "Customer first name"
     1028msgstr "Nombre del cliente"
     1029
     1030msgid "Customer full name"
     1031msgstr "Nombre completo del cliente"
     1032
     1033msgid "Fixed value"
     1034msgstr "Valor fijo"
     1035
     1036msgid "No products with a usable image were found."
     1037msgstr "No se encontraron productos con una imagen utilizable."
     1038
     1039msgid "Sending..."
     1040msgstr "Enviando..."
     1041
     1042msgid "Please fill in all template variables."
     1043msgstr "Rellena todas las variables de la plantilla."
     1044
     1045msgid "This template needs a header image. Choose one from the media library, a product, or paste an https image link."
     1046msgstr "Esta plantilla necesita una imagen de cabecera. Elige una de la biblioteca de medios, de un producto o pega un enlace https."
     1047
     1048msgid "%d subscribers will receive this campaign."
     1049msgstr "%d suscriptores recibirán esta campaña."
     1050
     1051msgid "Send this campaign to %d subscribers now?"
     1052msgstr "¿Enviar esta campaña a %d suscriptores ahora?"
     1053
     1054msgid "Schedule this campaign to %d subscribers?"
     1055msgstr "¿Programar esta campaña para %d suscriptores?"
     1056
     1057msgid "Campaign launched. Messages are on their way."
     1058msgstr "Campaña lanzada. Los mensajes están en camino."
     1059
     1060msgid "Campaign scheduled."
     1061msgstr "Campaña programada."
     1062
     1063msgid "No campaigns yet."
     1064msgstr "Aún no hay campañas."
     1065
     1066msgid "Pause"
     1067msgstr "Pausar"
     1068
     1069msgid "Cancel"
     1070msgstr "Cancelar"
     1071
     1072msgid "Pause this campaign?"
     1073msgstr "¿Pausar esta campaña?"
     1074
     1075msgid "Cancel this scheduled campaign?"
     1076msgstr "¿Cancelar esta campaña programada?"
     1077
     1078msgid "Processing"
     1079msgstr "En proceso"
     1080
     1081msgid "Scheduled"
     1082msgstr "Programada"
     1083
     1084msgid "Completed"
     1085msgstr "Completada"
     1086
     1087msgid "Cancelled"
     1088msgstr "Cancelada"
     1089
     1090msgid "Paused"
     1091msgstr "En pausa"
     1092
     1093msgid "Opt-out footer (added automatically):"
     1094msgstr "Pie de baja (añadido automáticamente):"
     1095
     1096msgid "The message text must be between 15 and 1024 characters."
     1097msgstr "El texto del mensaje debe tener entre 15 y 1024 caracteres."
     1098
     1099msgid "Template submitted. Meta approval usually takes minutes to a few hours."
     1100msgstr "Plantilla enviada. La aprobación de Meta suele tardar de minutos a unas horas."
     1101
     1102msgid "A button needs both a label and a link."
     1103msgstr "Un botón necesita una etiqueta y un enlace."
     1104
     1105msgid "Could not submit the template. Please try again."
     1106msgstr "No se pudo enviar la plantilla. Inténtalo de nuevo."
     1107
     1108msgid "Could not load templates. Please try again."
     1109msgstr "No se pudieron cargar las plantillas. Inténtalo de nuevo."
     1110
     1111msgid "Test send failed. Please try again."
     1112msgstr "El envío de prueba falló. Inténtalo de nuevo."
     1113
     1114msgid "Could not launch the campaign. Please try again."
     1115msgstr "No se pudo lanzar la campaña. Inténtalo de nuevo."
     1116
     1117msgid "The action failed. Please try again."
     1118msgstr "La acción falló. Inténtalo de nuevo."
     1119
     1120msgid "Invalid request."
     1121msgstr "Solicitud no válida."
     1122
     1123msgid "Choose a valid date and time for the scheduled send."
     1124msgstr "Elige una fecha y hora válidas para el envío programado."
     1125
     1126msgid "A campaign was just launched. Please wait two minutes before launching another."
     1127msgstr "Se acaba de lanzar una campaña. Espera dos minutos antes de lanzar otra."
     1128
     1129msgid "Your audience is larger than 5,000 contacts. Please run this campaign from the GoUltra dashboard."
     1130msgstr "Tu audiencia supera los 5.000 contactos. Ejecuta esta campaña desde el panel de GoUltra."
     1131
     1132msgid "Your GoUltra account has more than one WhatsApp number connected, so the sending number must be chosen in the GoUltra dashboard for this store."
     1133msgstr "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
     1135msgid "Your GoUltra plan message allowance for this cycle has been reached. Upgrade your plan to send more."
     1136msgstr "Se alcanzó el límite de mensajes de tu plan de GoUltra para este ciclo. Mejora tu plan para enviar más."
     1137
     1138msgid "GoUltra for WooCommerce - WhatsApp Marketing, Notifications & Abandoned Cart"
     1139msgstr "GoUltra para WooCommerce - Marketing, notificaciones y carrito abandonado por WhatsApp"
     1140
     1141msgid "WooCommerce WhatsApp notifications: automated order, shipping and abandoned-cart messages, plus opt-in marketing campaigns, powered by GoUltra."
     1142msgstr "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
     1144msgid "Marketing Offer"
     1145msgstr "Oferta de marketing"
     1146
     1147msgid "Promotions and offers for opted-in subscribers"
     1148msgstr "Promociones y ofertas para suscriptores que optaron por participar"
     1149
     1150msgid "Recipients"
     1151msgstr "Destinatarios"
     1152
     1153msgid "All subscribers"
     1154msgstr "Todos los suscriptores"
     1155
     1156msgid "Choose specific contacts"
     1157msgstr "Elegir contactos especificos"
     1158
     1159msgid "Search name or number"
     1160msgstr "Buscar nombre o numero"
     1161
     1162msgid "Select all"
     1163msgstr "Seleccionar todo"
     1164
     1165msgid "Clear"
     1166msgstr "Borrar"
     1167
     1168msgid "Also send to my own number (%s) for a test"
     1169msgstr "Enviar tambien a mi propio numero (%s) como prueba"
     1170
     1171msgid "Also send to my own number for a test (set your admin number in Settings)"
     1172msgstr "Enviar tambien a mi propio numero como prueba (configura tu numero de administrador en Ajustes)"
     1173
     1174msgid "Need a marketing template?"
     1175msgstr "¿Necesitas una plantilla de marketing?"
     1176
     1177msgid "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."
     1178msgstr "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
     1180msgid "Open Templates"
     1181msgstr "Abrir plantillas"
     1182
     1183msgid "Delivery receipts and product sync"
     1184msgstr "Recibos de entrega y sincronizacion de productos"
     1185
     1186msgid "These work automatically once your store is connected. There is nothing to set up."
     1187msgstr "Esto funciona automaticamente una vez que tu tienda esta conectada. No hay nada que configurar."
     1188
     1189msgid "Connected and active."
     1190msgstr "Conectado y activo."
     1191
     1192msgid "Message delivery receipts appear as order notes, and your product catalog is available to GoUltra automatically. No secret or URL to copy anywhere."
     1193msgstr "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
     1195msgid "Technical details (for support)"
     1196msgstr "Detalles tecnicos (para soporte)"
     1197
     1198msgid "Webhook secret"
     1199msgstr "Secreto de webhook"
     1200
     1201msgid "The plugin shares the secret with GoUltra automatically on connect, so you never paste it anywhere."
     1202msgstr "El plugin comparte el secreto con GoUltra automaticamente al conectar, asi que nunca tienes que pegarlo en ningun sitio."
     1203
     1204msgid "All subscribers (%d)"
     1205msgstr "Todos los suscriptores (%d)"
     1206
     1207msgid "%d selected"
     1208msgstr "%d seleccionados"
     1209
     1210msgid "No subscribers yet."
     1211msgstr "Aun no hay suscriptores."
     1212
     1213msgid "No recipients selected. Choose subscribers, or add your own number to send yourself a test."
     1214msgstr "No hay destinatarios seleccionados. Elige suscriptores o anade tu propio numero para enviarte una prueba."
     1215
     1216msgid "%d recipients will receive this campaign."
     1217msgstr "%d destinatarios recibiran esta campana."
     1218
     1219msgid "Send this campaign to %d recipients now?"
     1220msgstr "¿Enviar esta campana a %d destinatarios ahora?"
     1221
     1222msgid "Schedule this campaign to %d recipients?"
     1223msgstr "¿Programar esta campana para %d destinatarios?"
     1224
     1225msgid "Add my number"
     1226msgstr "Anadir mi numero"
     1227
     1228msgid "Add to the list"
     1229msgstr "Anadir a la lista"
     1230
     1231msgid "Adding..."
     1232msgstr "Anadiendo..."
     1233
     1234msgid "Added to the list."
     1235msgstr "Anadido a la lista."
     1236
     1237msgid "Could not add the contact. Please try again."
     1238msgstr "No se pudo anadir el contacto. Intentalo de nuevo."
     1239
     1240msgid "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."
     1241msgstr "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
     1243msgid "Tip: to test on yourself, add your own number on the Marketing List screen, then select only it here."
     1244msgstr "Consejo: para probar contigo mismo, anade tu numero en la pantalla Lista de marketing y luego seleccionalo solo aqui."
     1245
     1246msgid "No recipients selected. Choose contacts from the list."
     1247msgstr "No hay destinatarios seleccionados. Elige contactos de la lista."
     1248
     1249msgid "No name"
     1250msgstr "Sin nombre"
     1251
     1252msgid "Today"
     1253msgstr "Hoy"
     1254
     1255msgid "Your GoUltra account is currently on hold, so messages cannot be sent. Please contact GoUltra support to resume."
     1256msgstr "Tu cuenta de GoUltra esta suspendida temporalmente, por lo que no se pueden enviar mensajes. Contacta con el soporte de GoUltra para reanudar."
     1257
     1258msgid "Edit name"
     1259msgstr "Editar nombre"
     1260
     1261msgid "Schedule campaign"
     1262msgstr "Programar campana"
     1263
     1264msgid "Will be sent on %1$s at %2$s (your store time)."
     1265msgstr "Se enviara el %1$s a las %2$s (hora de tu tienda)."
     1266
     1267msgid "Could not update the name. Please try again."
     1268msgstr "No se pudo actualizar el nombre. Intentalo de nuevo."
     1269
     1270msgid "GoUltra returned HTTP %d."
     1271msgstr "GoUltra devolvio HTTP %d."
     1272
     1273msgid "Template name"
     1274msgstr "Nombre de la plantilla"
     1275
     1276msgid "Lowercase letters, numbers and underscores only."
     1277msgstr "Solo letras minusculas, numeros y guiones bajos."
     1278
     1279msgid "Marketing (offers, promotions)"
     1280msgstr "Marketing (ofertas, promociones)"
     1281
     1282msgid "Utility (order, account updates)"
     1283msgstr "Utilidad (pedido, actualizaciones de cuenta)"
     1284
     1285msgid "No header"
     1286msgstr "Sin cabecera"
     1287
     1288msgid "Text"
     1289msgstr "Texto"
     1290
     1291msgid "Video"
     1292msgstr "Video"
     1293
     1294msgid "Document"
     1295msgstr "Documento"
     1296
     1297msgid "Header text"
     1298msgstr "Texto de la cabecera"
     1299
     1300msgid "You will choose the actual image/video/file when you send a campaign."
     1301msgstr "Elegiras la imagen/video/archivo real al enviar una campana."
     1302
     1303msgid "Note:"
     1304msgstr "Nota:"
     1305
     1306msgid "Utility templates are for transactional messages only (order, shipping, account). Promotional content will be rejected by Meta."
     1307msgstr "Las plantillas de utilidad son solo para mensajes transaccionales (pedido, envio, cuenta). Meta rechazara el contenido promocional."
     1308
     1309msgid "Insert a variable"
     1310msgstr "Insertar una variable"
     1311
     1312msgid "(filled in automatically at send time)"
     1313msgstr "(se rellena automaticamente al enviar)"
     1314
     1315msgid "Buttons"
     1316msgstr "Botones"
     1317
     1318msgid "(up to 10, optional)"
     1319msgstr "(hasta 10, opcional)"
     1320
     1321msgid "Add button"
     1322msgstr "Anadir boton"
     1323
     1324msgid "Opt-out method (required for marketing)"
     1325msgstr "Metodo de baja (obligatorio para marketing)"
     1326
     1327msgid "Footer text (recommended)"
     1328msgstr "Texto de pie (recomendado)"
     1329
     1330msgid "Unsubscribe button"
     1331msgstr "Boton de baja"
     1332
     1333msgid "Footer"
     1334msgstr "Pie"
     1335
     1336msgid "(small gray line, optional)"
     1337msgstr "(pequena linea gris, opcional)"
     1338
     1339msgid "Quick reply"
     1340msgstr "Respuesta rapida"
     1341
     1342msgid "URL link"
     1343msgstr "Enlace URL"
     1344
     1345msgid "Phone number"
     1346msgstr "Numero de telefono"
     1347
     1348msgid "Button text"
     1349msgstr "Texto del boton"
     1350
     1351msgid "A URL or phone button needs a value."
     1352msgstr "Un boton de URL o telefono necesita un valor."
     1353
     1354msgid "Remove"
     1355msgstr "Quitar"
     1356
     1357msgid "Two variables cannot be next to each other. Add some text between them (Meta rejects it otherwise)."
     1358msgstr "Dos variables no pueden estar juntas. Anade texto entre ellas (Meta lo rechaza)."
     1359
     1360msgid "The message text must be between 5 and 1024 characters."
     1361msgstr "El texto del mensaje debe tener entre 5 y 1024 caracteres."
     1362
     1363msgid "Meta rejected the template. %1$s (fallback: %2$s)"
     1364msgstr "Meta rechazo la plantilla. %1$s (alternativa: %2$s)"
     1365
     1366msgid "Unknown error."
     1367msgstr "Error desconocido."
     1368
     1369msgid "The template could not be submitted. %s"
     1370msgstr "No se pudo enviar la plantilla. %s"
     1371
     1372msgid "This number is not on your marketing list, so there is no name to update."
     1373msgstr "Este numero no esta en tu lista de marketing, por lo que no hay nombre que actualizar."
     1374
     1375msgid "Paste your API key (it starts with gk_) from the GoUltra Dashboard → Settings → API."
     1376msgstr "Pega tu clave API (empieza por gk_) desde el panel de GoUltra → Ajustes → API."
     1377
     1378msgid "Campaign name (optional)"
     1379msgstr "Nombre de la campaña (opcional)"
     1380
     1381msgid "e.g. Summer Sale - July"
     1382msgstr "p. ej. Rebajas de verano - julio"
     1383
     1384msgid "A name to recognize this campaign in your history."
     1385msgstr "Un nombre para reconocer esta campaña en tu historial."
     1386
     1387msgid "Upload / Media Library"
     1388msgstr "Subir / Biblioteca de medios"
     1389
     1390msgid "Image selected"
     1391msgstr "Imagen seleccionada"
     1392
     1393msgid "Or paste an image URL"
     1394msgstr "O pega la URL de una imagen"
     1395
     1396msgid "JPEG or PNG images only. Choose from your Media Library, pick a store product, or paste a URL."
     1397msgstr "Solo imágenes JPEG o PNG. Elige de tu biblioteca de medios, un producto de la tienda o pega una URL."
     1398
     1399msgid "Enter a valid WhatsApp number, including the country code."
     1400msgstr "Introduce un número de WhatsApp válido, incluido el código de país."
     1401
     1402msgid "Too many code requests. Please wait a while before trying again."
     1403msgstr "Demasiadas solicitudes de código. Espera un poco antes de volver a intentarlo."
     1404
     1405msgid "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."
     1406msgstr "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
     1408msgid "Could not send the code. Please check the number and try again."
     1409msgstr "No se pudo enviar el código. Comprueba el número e inténtalo de nuevo."
     1410
     1411msgid "We sent a 6-digit code to %s on WhatsApp. Enter it below."
     1412msgstr "Enviamos un código de 6 dígitos a %s por WhatsApp. Introdúcelo abajo."
     1413
     1414msgid "The code expired. Please request a new one."
     1415msgstr "El código caducó. Solicita uno nuevo."
     1416
     1417msgid "Too many wrong attempts. Please request a new code."
     1418msgstr "Demasiados intentos incorrectos. Solicita un código nuevo."
     1419
     1420msgid "Incorrect code. Attempts left: %d."
     1421msgstr "Código incorrecto. Intentos restantes: %d."
     1422
     1423msgid "Your number is verified. It is now your store manager number and can be selected as a campaign recipient for your own tests."
     1424msgstr "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
     1426msgid "Store manager"
     1427msgstr "Administrador de la tienda"
     1428
     1429msgid "Verify your number (recommended)"
     1430msgstr "Verifica tu número (recomendado)"
     1431
     1432msgid "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."
     1433msgstr "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
     1435msgid "Verified"
     1436msgstr "Verificado"
     1437
     1438msgid "Verify a different number"
     1439msgstr "Verificar otro número"
     1440
     1441msgid "Send code"
     1442msgstr "Enviar código"
     1443
     1444msgid "Verify"
     1445msgstr "Verificar"
     1446
     1447msgid "Resend code"
     1448msgstr "Reenviar código"
     1449
     1450msgid "Verifying..."
     1451msgstr "Verificando..."
     1452
     1453msgid "Enter your WhatsApp number first."
     1454msgstr "Introduce primero tu número de WhatsApp."
     1455
     1456msgid "Enter the code you received."
     1457msgstr "Introduce el código que recibiste."
     1458
     1459msgid "Could not send the code."
     1460msgstr "No se pudo enviar el código."
     1461
     1462msgid "Incorrect code."
     1463msgstr "Código incorrecto."
     1464
     1465msgid "Network error. Please try again."
     1466msgstr "Error de red. Inténtalo de nuevo."
     1467
     1468msgid "Building a template for a product?"
     1469msgstr "¿Creas una plantilla para un producto?"
     1470
     1471msgid "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."
     1472msgstr "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
     1474msgid "View product"
     1475msgstr "Ver producto"
     1476
     1477msgid "No products found."
     1478msgstr "No se encontraron productos."
     1479
     1480msgid "You already have an approved or pending template of this type for this language. Create another one?"
     1481msgstr "Ya tienes una plantilla aprobada o pendiente de este tipo para este idioma. ¿Crear otra?"
     1482
     1483msgid "Unconfirmed - verify before resending"
     1484msgstr "Sin confirmar - verifica antes de reenviar"
     1485
     1486msgid "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."
     1487msgstr "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
     1489msgid "GoUltra for WooCommerce: connection problem."
     1490msgstr "GoUltra para WooCommerce: problema de conexion."
     1491
     1492msgid "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."
     1493msgstr "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
     1495msgid "Reconnect your store"
     1496msgstr "Vuelve a conectar tu tienda"
     1497
     1498msgid "Image"
     1499msgstr "Imagen"
     1500
     1501msgid "View"
     1502msgstr "Ver"
     1503
     1504msgid "rejected"
     1505msgstr "rechazada"
     1506
     1507msgid "No templates match your search."
     1508msgstr "Ninguna plantilla coincide con tu busqueda."
     1509
     1510msgid "Search your templates by name, text or language..."
     1511msgstr "Busca tus plantillas por nombre, texto o idioma..."
     1512
     1513msgid "Close"
     1514msgstr "Cerrar"
     1515
     1516msgid "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."
     1517msgstr "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
     1519msgid "JPEG or PNG images only."
     1520msgstr "Solo imagenes JPEG o PNG."
     1521
     1522msgid "Marketing sending is turned off for this WhatsApp number. Enable marketing for this number in your GoUltra dashboard to send campaigns."
     1523msgstr "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
     1525msgid "Chat with us"
     1526msgstr "Chatea con nosotros"
     1527
     1528msgid "Chat on WhatsApp"
     1529msgstr "Chatea por WhatsApp"
     1530
     1531msgid "about %s"
     1532msgstr "sobre %s"
     1533
     1534msgid "Ask about this product on WhatsApp"
     1535msgstr "Pregunta por este producto en WhatsApp"
     1536
     1537msgid "WhatsApp Chat Button"
     1538msgstr "Botón de chat de WhatsApp"
     1539
     1540msgid "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."
     1541msgstr "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
     1543msgid "Show the floating chat button"
     1544msgstr "Mostrar el botón de chat flotante"
     1545
     1546msgid "A round WhatsApp button in the corner of your storefront."
     1547msgstr "Un botón redondo de WhatsApp en la esquina de tu tienda."
     1548
     1549msgid "Include the country code. This is the number that will receive the chats."
     1550msgstr "Incluye el código de país. Este es el número que recibirá los chats."
     1551
     1552msgid "Pre-filled message"
     1553msgstr "Mensaje predefinido"
     1554
     1555msgid "Hi! I have a question about your store."
     1556msgstr "¡Hola! Tengo una pregunta sobre tu tienda."
     1557
     1558msgid "The message the customer starts with. Leave empty for none."
     1559msgstr "El mensaje con el que empieza el cliente. Déjalo vacío para ninguno."
     1560
     1561msgid "Button label"
     1562msgstr "Etiqueta del botón"
     1563
     1564msgid "Position"
     1565msgstr "Posición"
     1566
     1567msgid "Bottom right"
     1568msgstr "Abajo a la derecha"
     1569
     1570msgid "Bottom left"
     1571msgstr "Abajo a la izquierda"
     1572
     1573msgid "Show on"
     1574msgstr "Mostrar en"
     1575
     1576msgid "All pages"
     1577msgstr "Todas las páginas"
     1578
     1579msgid "Shop and product pages only"
     1580msgstr "Solo páginas de tienda y producto"
     1581
     1582msgid "Add a button on product pages"
     1583msgstr "Añadir un botón en las páginas de producto"
     1584
     1585msgid "Shows an \"Ask about this product\" WhatsApp button under Add to cart, with the product name filled in."
     1586msgstr "Muestra un botón de WhatsApp \"Pregunta por este producto\" debajo de Añadir al carrito, con el nombre del producto."
     1587
     1588msgid "Tip: you can also place a button anywhere with the shortcode"
     1589msgstr "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  
    913913msgid "Send a test message (optional)"
    914914msgstr "Envoyer un message de test (facultatif)"
     915
     916msgid "Campaigns"
     917msgstr "Campagnes"
     918
     919msgid "New campaign"
     920msgstr "Nouvelle campagne"
     921
     922msgid "Compose a WhatsApp campaign for your Marketing List subscribers. Unsubscribed contacts are always filtered out automatically."
     923msgstr "Composez une campagne WhatsApp pour les abonnés de votre liste marketing. Les contacts désabonnés sont toujours filtrés automatiquement."
     924
     925msgid "Connect your store to GoUltra to send campaigns."
     926msgstr "Connectez votre boutique à GoUltra pour envoyer des campagnes."
     927
     928msgid "Template"
     929msgstr "Modèle"
     930
     931msgid "Refresh"
     932msgstr "Actualiser"
     933
     934msgid "No approved marketing templates yet. Create one below."
     935msgstr "Aucun modèle marketing approuvé pour le moment. Créez-en un ci-dessous."
     936
     937msgid "Variables"
     938msgstr "Variables"
     939
     940msgid "Header image"
     941msgstr "Image d'en-tête"
     942
     943msgid "Media Library"
     944msgstr "Médiathèque"
     945
     946msgid "Pick a product"
     947msgstr "Choisir un produit"
     948
     949msgid "Search products"
     950msgstr "Rechercher des produits"
     951
     952msgid "Send a test to your phone first"
     953msgstr "Envoyez d'abord un test sur votre téléphone"
     954
     955msgid "Send test"
     956msgstr "Envoyer un test"
     957
     958msgid "Test sent. Check your WhatsApp."
     959msgstr "Test envoyé. Vérifiez votre WhatsApp."
     960
     961msgid "Audience"
     962msgstr "Audience"
     963
     964msgid "When to send"
     965msgstr "Quand envoyer"
     966
     967msgid "Send now"
     968msgstr "Envoyer maintenant"
     969
     970msgid "Schedule for later"
     971msgstr "Programmer pour plus tard"
     972
     973msgid "Launch campaign"
     974msgstr "Lancer la campagne"
     975
     976msgid "online"
     977msgstr "en ligne"
     978
     979msgid "Choose a template to see the preview."
     980msgstr "Choisissez un modèle pour voir l'aperçu."
     981
     982msgid "Campaign history"
     983msgstr "Historique des campagnes"
     984
     985msgid "Sent"
     986msgstr "Envoyés"
     987
     988msgid "Failed"
     989msgstr "Échoués"
     990
     991msgid "Create a marketing template"
     992msgstr "Créer un modèle marketing"
     993
     994msgid "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."
     995msgstr "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
     997msgid "Language"
     998msgstr "Langue"
     999
     1000msgid "Style"
     1001msgstr "Style"
     1002
     1003msgid "Message text"
     1004msgstr "Texte du message"
     1005
     1006msgid "Button label (optional)"
     1007msgstr "Libellé du bouton (facultatif)"
     1008
     1009msgid "Button link (optional)"
     1010msgstr "Lien du bouton (facultatif)"
     1011
     1012msgid "Submit for approval"
     1013msgstr "Soumettre pour approbation"
     1014
     1015msgid "Something went wrong. Please try again."
     1016msgstr "Un problème est survenu. Veuillez réessayer."
     1017
     1018msgid "Choose a template"
     1019msgstr "Choisissez un modèle"
     1020
     1021msgid "Choose a template first."
     1022msgstr "Choisissez d'abord un modèle."
     1023
     1024msgid "Awaiting Meta approval:"
     1025msgstr "En attente de l'approbation de Meta :"
     1026
     1027msgid "Customer first name"
     1028msgstr "Prénom du client"
     1029
     1030msgid "Customer full name"
     1031msgstr "Nom complet du client"
     1032
     1033msgid "Fixed value"
     1034msgstr "Valeur fixe"
     1035
     1036msgid "No products with a usable image were found."
     1037msgstr "Aucun produit avec une image utilisable n'a été trouvé."
     1038
     1039msgid "Sending..."
     1040msgstr "Envoi..."
     1041
     1042msgid "Please fill in all template variables."
     1043msgstr "Veuillez remplir toutes les variables du modèle."
     1044
     1045msgid "This template needs a header image. Choose one from the media library, a product, or paste an https image link."
     1046msgstr "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
     1048msgid "%d subscribers will receive this campaign."
     1049msgstr "%d abonnés recevront cette campagne."
     1050
     1051msgid "Send this campaign to %d subscribers now?"
     1052msgstr "Envoyer cette campagne à %d abonnés maintenant ?"
     1053
     1054msgid "Schedule this campaign to %d subscribers?"
     1055msgstr "Programmer cette campagne pour %d abonnés ?"
     1056
     1057msgid "Campaign launched. Messages are on their way."
     1058msgstr "Campagne lancée. Les messages sont en route."
     1059
     1060msgid "Campaign scheduled."
     1061msgstr "Campagne programmée."
     1062
     1063msgid "No campaigns yet."
     1064msgstr "Aucune campagne pour le moment."
     1065
     1066msgid "Pause"
     1067msgstr "Pause"
     1068
     1069msgid "Cancel"
     1070msgstr "Annuler"
     1071
     1072msgid "Pause this campaign?"
     1073msgstr "Mettre cette campagne en pause ?"
     1074
     1075msgid "Cancel this scheduled campaign?"
     1076msgstr "Annuler cette campagne programmée ?"
     1077
     1078msgid "Processing"
     1079msgstr "En cours"
     1080
     1081msgid "Scheduled"
     1082msgstr "Programmée"
     1083
     1084msgid "Completed"
     1085msgstr "Terminée"
     1086
     1087msgid "Cancelled"
     1088msgstr "Annulée"
     1089
     1090msgid "Paused"
     1091msgstr "En pause"
     1092
     1093msgid "Opt-out footer (added automatically):"
     1094msgstr "Pied de désinscription (ajouté automatiquement) :"
     1095
     1096msgid "The message text must be between 15 and 1024 characters."
     1097msgstr "Le texte du message doit comporter entre 15 et 1024 caractères."
     1098
     1099msgid "Template submitted. Meta approval usually takes minutes to a few hours."
     1100msgstr "Modèle soumis. L'approbation de Meta prend généralement de quelques minutes à quelques heures."
     1101
     1102msgid "A button needs both a label and a link."
     1103msgstr "Un bouton nécessite un libellé et un lien."
     1104
     1105msgid "Could not submit the template. Please try again."
     1106msgstr "Impossible de soumettre le modèle. Veuillez réessayer."
     1107
     1108msgid "Could not load templates. Please try again."
     1109msgstr "Impossible de charger les modèles. Veuillez réessayer."
     1110
     1111msgid "Test send failed. Please try again."
     1112msgstr "L'envoi du test a échoué. Veuillez réessayer."
     1113
     1114msgid "Could not launch the campaign. Please try again."
     1115msgstr "Impossible de lancer la campagne. Veuillez réessayer."
     1116
     1117msgid "The action failed. Please try again."
     1118msgstr "L'action a échoué. Veuillez réessayer."
     1119
     1120msgid "Invalid request."
     1121msgstr "Requête non valide."
     1122
     1123msgid "Choose a valid date and time for the scheduled send."
     1124msgstr "Choisissez une date et une heure valides pour l'envoi programmé."
     1125
     1126msgid "A campaign was just launched. Please wait two minutes before launching another."
     1127msgstr "Une campagne vient d'être lancée. Attendez deux minutes avant d'en lancer une autre."
     1128
     1129msgid "Your audience is larger than 5,000 contacts. Please run this campaign from the GoUltra dashboard."
     1130msgstr "Votre audience dépasse 5 000 contacts. Lancez cette campagne depuis le tableau de bord GoUltra."
     1131
     1132msgid "Your GoUltra account has more than one WhatsApp number connected, so the sending number must be chosen in the GoUltra dashboard for this store."
     1133msgstr "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
     1135msgid "Your GoUltra plan message allowance for this cycle has been reached. Upgrade your plan to send more."
     1136msgstr "Le quota de messages de votre forfait GoUltra pour ce cycle est atteint. Améliorez votre forfait pour en envoyer plus."
     1137
     1138msgid "GoUltra for WooCommerce - WhatsApp Marketing, Notifications & Abandoned Cart"
     1139msgstr "GoUltra pour WooCommerce - Marketing, notifications et panier abandonné par WhatsApp"
     1140
     1141msgid "WooCommerce WhatsApp notifications: automated order, shipping and abandoned-cart messages, plus opt-in marketing campaigns, powered by GoUltra."
     1142msgstr "Notifications WhatsApp pour WooCommerce : messages automatisés de commande, de livraison et de panier abandonné, plus des campagnes marketing avec consentement, avec GoUltra."
     1143
     1144msgid "Marketing Offer"
     1145msgstr "Offre marketing"
     1146
     1147msgid "Promotions and offers for opted-in subscribers"
     1148msgstr "Promotions et offres pour les abonnes inscrits"
     1149
     1150msgid "Recipients"
     1151msgstr "Destinataires"
     1152
     1153msgid "All subscribers"
     1154msgstr "Tous les abonnes"
     1155
     1156msgid "Choose specific contacts"
     1157msgstr "Choisir des contacts specifiques"
     1158
     1159msgid "Search name or number"
     1160msgstr "Rechercher un nom ou un numero"
     1161
     1162msgid "Select all"
     1163msgstr "Tout selectionner"
     1164
     1165msgid "Clear"
     1166msgstr "Effacer"
     1167
     1168msgid "Also send to my own number (%s) for a test"
     1169msgstr "Envoyer aussi a mon propre numero (%s) pour un test"
     1170
     1171msgid "Also send to my own number for a test (set your admin number in Settings)"
     1172msgstr "Envoyer aussi a mon propre numero pour un test (definissez votre numero admin dans les reglages)"
     1173
     1174msgid "Need a marketing template?"
     1175msgstr "Besoin d'un modele marketing ?"
     1176
     1177msgid "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."
     1178msgstr "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
     1180msgid "Open Templates"
     1181msgstr "Ouvrir les modeles"
     1182
     1183msgid "Delivery receipts and product sync"
     1184msgstr "Accuses de reception et synchronisation produits"
     1185
     1186msgid "These work automatically once your store is connected. There is nothing to set up."
     1187msgstr "Cela fonctionne automatiquement une fois votre boutique connectee. Il n'y a rien a configurer."
     1188
     1189msgid "Connected and active."
     1190msgstr "Connecte et actif."
     1191
     1192msgid "Message delivery receipts appear as order notes, and your product catalog is available to GoUltra automatically. No secret or URL to copy anywhere."
     1193msgstr "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
     1195msgid "Technical details (for support)"
     1196msgstr "Details techniques (pour le support)"
     1197
     1198msgid "Webhook secret"
     1199msgstr "Secret du webhook"
     1200
     1201msgid "The plugin shares the secret with GoUltra automatically on connect, so you never paste it anywhere."
     1202msgstr "L'extension partage le secret avec GoUltra automatiquement a la connexion, vous n'avez donc jamais a le coller nulle part."
     1203
     1204msgid "All subscribers (%d)"
     1205msgstr "Tous les abonnes (%d)"
     1206
     1207msgid "%d selected"
     1208msgstr "%d selectionnes"
     1209
     1210msgid "No subscribers yet."
     1211msgstr "Aucun abonne pour l'instant."
     1212
     1213msgid "No recipients selected. Choose subscribers, or add your own number to send yourself a test."
     1214msgstr "Aucun destinataire selectionne. Choisissez des abonnes, ou ajoutez votre propre numero pour vous envoyer un test."
     1215
     1216msgid "%d recipients will receive this campaign."
     1217msgstr "%d destinataires recevront cette campagne."
     1218
     1219msgid "Send this campaign to %d recipients now?"
     1220msgstr "Envoyer cette campagne a %d destinataires maintenant ?"
     1221
     1222msgid "Schedule this campaign to %d recipients?"
     1223msgstr "Programmer cette campagne pour %d destinataires ?"
     1224
     1225msgid "Add my number"
     1226msgstr "Ajouter mon numero"
     1227
     1228msgid "Add to the list"
     1229msgstr "Ajouter a la liste"
     1230
     1231msgid "Adding..."
     1232msgstr "Ajout..."
     1233
     1234msgid "Added to the list."
     1235msgstr "Ajoute a la liste."
     1236
     1237msgid "Could not add the contact. Please try again."
     1238msgstr "Impossible d'ajouter le contact. Veuillez reessayer."
     1239
     1240msgid "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."
     1241msgstr "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
     1243msgid "Tip: to test on yourself, add your own number on the Marketing List screen, then select only it here."
     1244msgstr "Astuce : pour tester sur vous-meme, ajoutez votre numero sur l'ecran Liste marketing, puis selectionnez-le uniquement ici."
     1245
     1246msgid "No recipients selected. Choose contacts from the list."
     1247msgstr "Aucun destinataire selectionne. Choisissez des contacts dans la liste."
     1248
     1249msgid "No name"
     1250msgstr "Sans nom"
     1251
     1252msgid "Today"
     1253msgstr "Aujourd'hui"
     1254
     1255msgid "Your GoUltra account is currently on hold, so messages cannot be sent. Please contact GoUltra support to resume."
     1256msgstr "Votre compte GoUltra est actuellement suspendu, les messages ne peuvent donc pas etre envoyes. Contactez le support GoUltra pour reprendre."
     1257
     1258msgid "Edit name"
     1259msgstr "Modifier le nom"
     1260
     1261msgid "Schedule campaign"
     1262msgstr "Programmer la campagne"
     1263
     1264msgid "Will be sent on %1$s at %2$s (your store time)."
     1265msgstr "Sera envoye le %1$s a %2$s (heure de votre boutique)."
     1266
     1267msgid "Could not update the name. Please try again."
     1268msgstr "Impossible de mettre a jour le nom. Veuillez reessayer."
     1269
     1270msgid "GoUltra returned HTTP %d."
     1271msgstr "GoUltra a renvoye HTTP %d."
     1272
     1273msgid "Template name"
     1274msgstr "Nom du modele"
     1275
     1276msgid "Lowercase letters, numbers and underscores only."
     1277msgstr "Lettres minuscules, chiffres et traits de soulignement uniquement."
     1278
     1279msgid "Marketing (offers, promotions)"
     1280msgstr "Marketing (offres, promotions)"
     1281
     1282msgid "Utility (order, account updates)"
     1283msgstr "Utilitaire (commande, mises a jour du compte)"
     1284
     1285msgid "No header"
     1286msgstr "Sans en-tete"
     1287
     1288msgid "Text"
     1289msgstr "Texte"
     1290
     1291msgid "Video"
     1292msgstr "Video"
     1293
     1294msgid "Document"
     1295msgstr "Document"
     1296
     1297msgid "Header text"
     1298msgstr "Texte de l'en-tete"
     1299
     1300msgid "You will choose the actual image/video/file when you send a campaign."
     1301msgstr "Vous choisirez l'image/video/fichier reel lors de l'envoi d'une campagne."
     1302
     1303msgid "Note:"
     1304msgstr "Remarque :"
     1305
     1306msgid "Utility templates are for transactional messages only (order, shipping, account). Promotional content will be rejected by Meta."
     1307msgstr "Les modeles utilitaires sont reserves aux messages transactionnels (commande, expedition, compte). Meta rejettera tout contenu promotionnel."
     1308
     1309msgid "Insert a variable"
     1310msgstr "Inserer une variable"
     1311
     1312msgid "(filled in automatically at send time)"
     1313msgstr "(rempli automatiquement a l'envoi)"
     1314
     1315msgid "Buttons"
     1316msgstr "Boutons"
     1317
     1318msgid "(up to 10, optional)"
     1319msgstr "(jusqu'a 10, facultatif)"
     1320
     1321msgid "Add button"
     1322msgstr "Ajouter un bouton"
     1323
     1324msgid "Opt-out method (required for marketing)"
     1325msgstr "Methode de desinscription (obligatoire pour le marketing)"
     1326
     1327msgid "Footer text (recommended)"
     1328msgstr "Texte de pied (recommande)"
     1329
     1330msgid "Unsubscribe button"
     1331msgstr "Bouton de desinscription"
     1332
     1333msgid "Footer"
     1334msgstr "Pied"
     1335
     1336msgid "(small gray line, optional)"
     1337msgstr "(petite ligne grise, facultatif)"
     1338
     1339msgid "Quick reply"
     1340msgstr "Reponse rapide"
     1341
     1342msgid "URL link"
     1343msgstr "Lien URL"
     1344
     1345msgid "Phone number"
     1346msgstr "Numero de telephone"
     1347
     1348msgid "Button text"
     1349msgstr "Texte du bouton"
     1350
     1351msgid "A URL or phone button needs a value."
     1352msgstr "Un bouton URL ou telephone necessite une valeur."
     1353
     1354msgid "Remove"
     1355msgstr "Retirer"
     1356
     1357msgid "Two variables cannot be next to each other. Add some text between them (Meta rejects it otherwise)."
     1358msgstr "Deux variables ne peuvent pas etre cote a cote. Ajoutez du texte entre elles (sinon Meta rejette)."
     1359
     1360msgid "The message text must be between 5 and 1024 characters."
     1361msgstr "Le texte du message doit comporter entre 5 et 1024 caracteres."
     1362
     1363msgid "Meta rejected the template. %1$s (fallback: %2$s)"
     1364msgstr "Meta a rejete le modele. %1$s (repli : %2$s)"
     1365
     1366msgid "Unknown error."
     1367msgstr "Erreur inconnue."
     1368
     1369msgid "The template could not be submitted. %s"
     1370msgstr "Le modele n'a pas pu etre soumis. %s"
     1371
     1372msgid "This number is not on your marketing list, so there is no name to update."
     1373msgstr "Ce numero n'est pas dans votre liste marketing, il n'y a donc aucun nom a mettre a jour."
     1374
     1375msgid "Paste your API key (it starts with gk_) from the GoUltra Dashboard → Settings → API."
     1376msgstr "Collez votre clé API (elle commence par gk_) depuis le tableau de bord GoUltra → Réglages → API."
     1377
     1378msgid "Campaign name (optional)"
     1379msgstr "Nom de la campagne (facultatif)"
     1380
     1381msgid "e.g. Summer Sale - July"
     1382msgstr "ex. Soldes d'ete - juillet"
     1383
     1384msgid "A name to recognize this campaign in your history."
     1385msgstr "Un nom pour reconnaitre cette campagne dans votre historique."
     1386
     1387msgid "Upload / Media Library"
     1388msgstr "Televerser / Mediatheque"
     1389
     1390msgid "Image selected"
     1391msgstr "Image selectionnee"
     1392
     1393msgid "Or paste an image URL"
     1394msgstr "Ou collez l'URL d'une image"
     1395
     1396msgid "JPEG or PNG images only. Choose from your Media Library, pick a store product, or paste a URL."
     1397msgstr "Images JPEG ou PNG uniquement. Choisissez dans la mediatheque, selectionnez un produit ou collez une URL."
     1398
     1399msgid "Enter a valid WhatsApp number, including the country code."
     1400msgstr "Saisissez un numéro WhatsApp valide, avec l'indicatif du pays."
     1401
     1402msgid "Too many code requests. Please wait a while before trying again."
     1403msgstr "Trop de demandes de code. Patientez un moment avant de réessayer."
     1404
     1405msgid "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."
     1406msgstr "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
     1408msgid "Could not send the code. Please check the number and try again."
     1409msgstr "Impossible d'envoyer le code. Vérifiez le numéro et réessayez."
     1410
     1411msgid "We sent a 6-digit code to %s on WhatsApp. Enter it below."
     1412msgstr "Nous avons envoyé un code à 6 chiffres au %s sur WhatsApp. Saisissez-le ci-dessous."
     1413
     1414msgid "The code expired. Please request a new one."
     1415msgstr "Le code a expiré. Demandez-en un nouveau."
     1416
     1417msgid "Too many wrong attempts. Please request a new code."
     1418msgstr "Trop de tentatives incorrectes. Demandez un nouveau code."
     1419
     1420msgid "Incorrect code. Attempts left: %d."
     1421msgstr "Code incorrect. Tentatives restantes : %d."
     1422
     1423msgid "Your number is verified. It is now your store manager number and can be selected as a campaign recipient for your own tests."
     1424msgstr "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
     1426msgid "Store manager"
     1427msgstr "Responsable de la boutique"
     1428
     1429msgid "Verify your number (recommended)"
     1430msgstr "Vérifiez votre numéro (recommandé)"
     1431
     1432msgid "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."
     1433msgstr "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
     1435msgid "Verified"
     1436msgstr "Vérifié"
     1437
     1438msgid "Verify a different number"
     1439msgstr "Vérifier un autre numéro"
     1440
     1441msgid "Send code"
     1442msgstr "Envoyer le code"
     1443
     1444msgid "Verify"
     1445msgstr "Vérifier"
     1446
     1447msgid "Resend code"
     1448msgstr "Renvoyer le code"
     1449
     1450msgid "Verifying..."
     1451msgstr "Vérification..."
     1452
     1453msgid "Enter your WhatsApp number first."
     1454msgstr "Saisissez d'abord votre numéro WhatsApp."
     1455
     1456msgid "Enter the code you received."
     1457msgstr "Saisissez le code que vous avez reçu."
     1458
     1459msgid "Could not send the code."
     1460msgstr "Impossible d'envoyer le code."
     1461
     1462msgid "Incorrect code."
     1463msgstr "Code incorrect."
     1464
     1465msgid "Network error. Please try again."
     1466msgstr "Erreur réseau. Réessayez."
     1467
     1468msgid "Building a template for a product?"
     1469msgstr "Vous créez un modèle pour un produit ?"
     1470
     1471msgid "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."
     1472msgstr "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
     1474msgid "View product"
     1475msgstr "Voir le produit"
     1476
     1477msgid "No products found."
     1478msgstr "Aucun produit trouvé."
     1479
     1480msgid "You already have an approved or pending template of this type for this language. Create another one?"
     1481msgstr "Vous avez déjà un modèle approuvé ou en attente de ce type pour cette langue. En créer un autre ?"
     1482
     1483msgid "Unconfirmed - verify before resending"
     1484msgstr "Non confirme - verifiez avant de renvoyer"
     1485
     1486msgid "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."
     1487msgstr "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
     1489msgid "GoUltra for WooCommerce: connection problem."
     1490msgstr "GoUltra pour WooCommerce : probleme de connexion."
     1491
     1492msgid "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."
     1493msgstr "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
     1495msgid "Reconnect your store"
     1496msgstr "Reconnectez votre boutique"
     1497
     1498msgid "Image"
     1499msgstr "Image"
     1500
     1501msgid "View"
     1502msgstr "Voir"
     1503
     1504msgid "rejected"
     1505msgstr "refusee"
     1506
     1507msgid "No templates match your search."
     1508msgstr "Aucun modele ne correspond a votre recherche."
     1509
     1510msgid "Search your templates by name, text or language..."
     1511msgstr "Recherchez vos modeles par nom, texte ou langue..."
     1512
     1513msgid "Close"
     1514msgstr "Fermer"
     1515
     1516msgid "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."
     1517msgstr "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
     1519msgid "JPEG or PNG images only."
     1520msgstr "Images JPEG ou PNG uniquement."
     1521
     1522msgid "Marketing sending is turned off for this WhatsApp number. Enable marketing for this number in your GoUltra dashboard to send campaigns."
     1523msgstr "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
     1525msgid "Chat with us"
     1526msgstr "Discutez avec nous"
     1527
     1528msgid "Chat on WhatsApp"
     1529msgstr "Discuter sur WhatsApp"
     1530
     1531msgid "about %s"
     1532msgstr "à propos de %s"
     1533
     1534msgid "Ask about this product on WhatsApp"
     1535msgstr "Poser une question sur ce produit sur WhatsApp"
     1536
     1537msgid "WhatsApp Chat Button"
     1538msgstr "Bouton de chat WhatsApp"
     1539
     1540msgid "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."
     1541msgstr "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
     1543msgid "Show the floating chat button"
     1544msgstr "Afficher le bouton de chat flottant"
     1545
     1546msgid "A round WhatsApp button in the corner of your storefront."
     1547msgstr "Un bouton WhatsApp rond dans le coin de votre boutique."
     1548
     1549msgid "Include the country code. This is the number that will receive the chats."
     1550msgstr "Incluez l'indicatif du pays. C'est le numéro qui recevra les discussions."
     1551
     1552msgid "Pre-filled message"
     1553msgstr "Message pré-rempli"
     1554
     1555msgid "Hi! I have a question about your store."
     1556msgstr "Bonjour ! J'ai une question sur votre boutique."
     1557
     1558msgid "The message the customer starts with. Leave empty for none."
     1559msgstr "Le message par lequel le client commence. Laissez vide pour aucun."
     1560
     1561msgid "Button label"
     1562msgstr "Libellé du bouton"
     1563
     1564msgid "Position"
     1565msgstr "Position"
     1566
     1567msgid "Bottom right"
     1568msgstr "En bas à droite"
     1569
     1570msgid "Bottom left"
     1571msgstr "En bas à gauche"
     1572
     1573msgid "Show on"
     1574msgstr "Afficher sur"
     1575
     1576msgid "All pages"
     1577msgstr "Toutes les pages"
     1578
     1579msgid "Shop and product pages only"
     1580msgstr "Pages boutique et produit uniquement"
     1581
     1582msgid "Add a button on product pages"
     1583msgstr "Ajouter un bouton sur les pages produit"
     1584
     1585msgid "Shows an \"Ask about this product\" WhatsApp button under Add to cart, with the product name filled in."
     1586msgstr "Affiche un bouton WhatsApp \"Poser une question sur ce produit\" sous Ajouter au panier, avec le nom du produit."
     1587
     1588msgid "Tip: you can also place a button anywhere with the shortcode"
     1589msgstr "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  
    2626
    2727msgid "Analytics"
    28 msgstr "אנליטיקס"
     28msgstr "דוחות"
    2929
    3030msgid "API key is required."
    … …  
    913913msgid "Send a test message (optional)"
    914914msgstr "שליחת הודעת בדיקה (אופציונלי)"
     915
     916msgid "Campaigns"
     917msgstr "קמפיינים"
     918
     919msgid "New campaign"
     920msgstr "קמפיין חדש"
     921
     922msgid "Compose a WhatsApp campaign for your Marketing List subscribers. Unsubscribed contacts are always filtered out automatically."
     923msgstr "חברו קמפיין וואטסאפ לנמעני רשימת התפוצה שלכם. אנשי קשר שהוסרו מסוננים תמיד אוטומטית."
     924
     925msgid "Connect your store to GoUltra to send campaigns."
     926msgstr "חברו את החנות ל-GoUltra כדי לשלוח קמפיינים."
     927
     928msgid "Template"
     929msgstr "תבנית"
     930
     931msgid "Refresh"
     932msgstr "רענון"
     933
     934msgid "No approved marketing templates yet. Create one below."
     935msgstr "אין עדיין תבניות שיווקיות מאושרות. צרו אחת למטה."
     936
     937msgid "Variables"
     938msgstr "משתנים"
     939
     940msgid "Header image"
     941msgstr "תמונת כותרת"
     942
     943msgid "Media Library"
     944msgstr "ספריית המדיה"
     945
     946msgid "Pick a product"
     947msgstr "בחירת מוצר"
     948
     949msgid "Search products"
     950msgstr "חיפוש מוצרים"
     951
     952msgid "Send a test to your phone first"
     953msgstr "שלחו קודם בדיקה לטלפון שלכם"
     954
     955msgid "Send test"
     956msgstr "שליחת בדיקה"
     957
     958msgid "Test sent. Check your WhatsApp."
     959msgstr "הבדיקה נשלחה. בדקו את הוואטסאפ שלכם."
     960
     961msgid "Audience"
     962msgstr "קהל יעד"
     963
     964msgid "When to send"
     965msgstr "מתי לשלוח"
     966
     967msgid "Send now"
     968msgstr "לשלוח עכשיו"
     969
     970msgid "Schedule for later"
     971msgstr "לתזמן למועד אחר"
     972
     973msgid "Launch campaign"
     974msgstr "שיגור הקמפיין"
     975
     976msgid "online"
     977msgstr "מחובר/ת"
     978
     979msgid "Choose a template to see the preview."
     980msgstr "בחרו תבנית כדי לראות את התצוגה המקדימה."
     981
     982msgid "Campaign history"
     983msgstr "היסטוריית קמפיינים"
     984
     985msgid "Sent"
     986msgstr "נשלחו"
     987
     988msgid "Failed"
     989msgstr "נכשלו"
     990
     991msgid "Create a marketing template"
     992msgstr "יצירת תבנית שיווקית"
     993
     994msgid "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."
     995msgstr "בחרו שפה וסגנון, התאימו את הטקסט ושלחו לאישור Meta. שורת הסרה בשפת התבנית מתווספת אוטומטית, כפי ש-Meta דורשת לשיווק."
     996
     997msgid "Language"
     998msgstr "שפה"
     999
     1000msgid "Style"
     1001msgstr "סגנון"
     1002
     1003msgid "Message text"
     1004msgstr "טקסט ההודעה"
     1005
     1006msgid "Button label (optional)"
     1007msgstr "כיתוב כפתור (אופציונלי)"
     1008
     1009msgid "Button link (optional)"
     1010msgstr "קישור כפתור (אופציונלי)"
     1011
     1012msgid "Submit for approval"
     1013msgstr "שליחה לאישור"
     1014
     1015msgid "Something went wrong. Please try again."
     1016msgstr "משהו השתבש. נסו שוב."
     1017
     1018msgid "Choose a template"
     1019msgstr "בחרו תבנית"
     1020
     1021msgid "Choose a template first."
     1022msgstr "בחרו תבנית קודם."
     1023
     1024msgid "Awaiting Meta approval:"
     1025msgstr "ממתינות לאישור Meta:"
     1026
     1027msgid "Customer first name"
     1028msgstr "שם פרטי של הלקוח"
     1029
     1030msgid "Customer full name"
     1031msgstr "שם מלא של הלקוח"
     1032
     1033msgid "Fixed value"
     1034msgstr "ערך קבוע"
     1035
     1036msgid "No products with a usable image were found."
     1037msgstr "לא נמצאו מוצרים עם תמונה מתאימה."
     1038
     1039msgid "Sending..."
     1040msgstr "שולח..."
     1041
     1042msgid "Please fill in all template variables."
     1043msgstr "מלאו את כל משתני התבנית."
     1044
     1045msgid "This template needs a header image. Choose one from the media library, a product, or paste an https image link."
     1046msgstr "התבנית הזו צריכה תמונת כותרת. בחרו מספריית המדיה, ממוצר, או הדביקו קישור https לתמונה."
     1047
     1048msgid "%d subscribers will receive this campaign."
     1049msgstr "%d נמענים יקבלו את הקמפיין הזה."
     1050
     1051msgid "Send this campaign to %d subscribers now?"
     1052msgstr "לשלוח את הקמפיין ל-%d נמענים עכשיו?"
     1053
     1054msgid "Schedule this campaign to %d subscribers?"
     1055msgstr "לתזמן את הקמפיין ל-%d נמענים?"
     1056
     1057msgid "Campaign launched. Messages are on their way."
     1058msgstr "הקמפיין שוגר. ההודעות בדרך."
     1059
     1060msgid "Campaign scheduled."
     1061msgstr "הקמפיין תוזמן."
     1062
     1063msgid "No campaigns yet."
     1064msgstr "אין עדיין קמפיינים."
     1065
     1066msgid "Pause"
     1067msgstr "השהיה"
     1068
     1069msgid "Cancel"
     1070msgstr "ביטול"
     1071
     1072msgid "Pause this campaign?"
     1073msgstr "להשהות את הקמפיין?"
     1074
     1075msgid "Cancel this scheduled campaign?"
     1076msgstr "לבטל את הקמפיין המתוזמן?"
     1077
     1078msgid "Processing"
     1079msgstr "בתהליך"
     1080
     1081msgid "Scheduled"
     1082msgstr "מתוזמן"
     1083
     1084msgid "Completed"
     1085msgstr "הושלם"
     1086
     1087msgid "Cancelled"
     1088msgstr "בוטל"
     1089
     1090msgid "Paused"
     1091msgstr "מושהה"
     1092
     1093msgid "Opt-out footer (added automatically):"
     1094msgstr "שורת הסרה (מתווספת אוטומטית):"
     1095
     1096msgid "The message text must be between 15 and 1024 characters."
     1097msgstr "טקסט ההודעה חייב להיות בין 15 ל-1024 תווים."
     1098
     1099msgid "Template submitted. Meta approval usually takes minutes to a few hours."
     1100msgstr "התבנית נשלחה. אישור Meta לוקח בדרך כלל בין דקות לכמה שעות."
     1101
     1102msgid "A button needs both a label and a link."
     1103msgstr "כפתור צריך גם כיתוב וגם קישור."
     1104
     1105msgid "Could not submit the template. Please try again."
     1106msgstr "לא ניתן לשלוח את התבנית. נסו שוב."
     1107
     1108msgid "Could not load templates. Please try again."
     1109msgstr "לא ניתן לטעון את התבניות. נסו שוב."
     1110
     1111msgid "Test send failed. Please try again."
     1112msgstr "שליחת הבדיקה נכשלה. נסו שוב."
     1113
     1114msgid "Could not launch the campaign. Please try again."
     1115msgstr "לא ניתן לשגר את הקמפיין. נסו שוב."
     1116
     1117msgid "The action failed. Please try again."
     1118msgstr "הפעולה נכשלה. נסו שוב."
     1119
     1120msgid "Invalid request."
     1121msgstr "בקשה לא תקינה."
     1122
     1123msgid "Choose a valid date and time for the scheduled send."
     1124msgstr "בחרו תאריך ושעה תקינים לשליחה המתוזמנת."
     1125
     1126msgid "A campaign was just launched. Please wait two minutes before launching another."
     1127msgstr "קמפיין שוגר ממש עכשיו. המתינו שתי דקות לפני שיגור נוסף."
     1128
     1129msgid "Your audience is larger than 5,000 contacts. Please run this campaign from the GoUltra dashboard."
     1130msgstr "קהל היעד גדול מ-5,000 אנשי קשר. הריצו את הקמפיין הזה מלוח הבקרה של GoUltra."
     1131
     1132msgid "Your GoUltra account has more than one WhatsApp number connected, so the sending number must be chosen in the GoUltra dashboard for this store."
     1133msgstr "לחשבון ה-GoUltra שלכם מחוברים כמה מספרי וואטסאפ, ולכן יש לבחור את מספר השליחה של החנות בלוח הבקרה של GoUltra."
     1134
     1135msgid "Your GoUltra plan message allowance for this cycle has been reached. Upgrade your plan to send more."
     1136msgstr "מכסת ההודעות של מסלול ה-GoUltra שלכם למחזור הזה נוצלה. שדרגו את המסלול כדי לשלוח עוד."
     1137
     1138msgid "GoUltra for WooCommerce - WhatsApp Marketing, Notifications & Abandoned Cart"
     1139msgstr "GoUltra ל-WooCommerce - שיווק, התראות ועגלה נטושה בוואטסאפ"
     1140
     1141msgid "WooCommerce WhatsApp notifications: automated order, shipping and abandoned-cart messages, plus opt-in marketing campaigns, powered by GoUltra."
     1142msgstr "התראות WhatsApp ל-WooCommerce: הודעות אוטומטיות לאישור הזמנה, משלוח ועגלה נטושה, וקמפיינים שיווקיים בהסכמה, באמצעות GoUltra."
     1143
     1144msgid "Marketing Offer"
     1145msgstr "מבצע שיווקי"
     1146
     1147msgid "Promotions and offers for opted-in subscribers"
     1148msgstr "מבצעים והצעות לנמענים שנרשמו"
     1149
     1150msgid "Recipients"
     1151msgstr "נמענים"
     1152
     1153msgid "All subscribers"
     1154msgstr "כל הנמענים"
     1155
     1156msgid "Choose specific contacts"
     1157msgstr "בחירת אנשי קשר מסוימים"
     1158
     1159msgid "Search name or number"
     1160msgstr "חיפוש שם או מספר"
     1161
     1162msgid "Select all"
     1163msgstr "בחירת הכל"
     1164
     1165msgid "Clear"
     1166msgstr "ניקוי"
     1167
     1168msgid "Also send to my own number (%s) for a test"
     1169msgstr "לשלוח גם למספר שלי (%s) לבדיקה"
     1170
     1171msgid "Also send to my own number for a test (set your admin number in Settings)"
     1172msgstr "לשלוח גם למספר שלי לבדיקה (הגדירו את מספר המנהל בהגדרות)"
     1173
     1174msgid "Need a marketing template?"
     1175msgstr "צריכים תבנית שיווקית?"
     1176
     1177msgid "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."
     1178msgstr "צרו אותה בלשונית תבניות (מבצע שיווקי): בחרו שפה וסגנון מוכן, ושורת ההסרה מתווספת אוטומטית. תבניות מאושרות מופיעות בבורר למעלה."
     1179
     1180msgid "Open Templates"
     1181msgstr "פתיחת תבניות"
     1182
     1183msgid "Delivery receipts and product sync"
     1184msgstr "אישורי מסירה וסנכרון מוצרים"
     1185
     1186msgid "These work automatically once your store is connected. There is nothing to set up."
     1187msgstr "אלה עובדים אוטומטית לאחר חיבור החנות. אין מה להגדיר."
     1188
     1189msgid "Connected and active."
     1190msgstr "מחובר ופעיל."
     1191
     1192msgid "Message delivery receipts appear as order notes, and your product catalog is available to GoUltra automatically. No secret or URL to copy anywhere."
     1193msgstr "אישורי מסירת הודעות מופיעים כהערות בהזמנה, וקטלוג המוצרים שלכם זמין ל-GoUltra אוטומטית. אין סוד או כתובת להעתיק."
     1194
     1195msgid "Technical details (for support)"
     1196msgstr "פרטים טכניים (לתמיכה)"
     1197
     1198msgid "Webhook secret"
     1199msgstr "סוד Webhook"
     1200
     1201msgid "The plugin shares the secret with GoUltra automatically on connect, so you never paste it anywhere."
     1202msgstr "התוסף משתף את הסוד עם GoUltra אוטומטית בעת החיבור, כך שאתם לא צריכים להדביק אותו בשום מקום."
     1203
     1204msgid "All subscribers (%d)"
     1205msgstr "כל הנמענים (%d)"
     1206
     1207msgid "%d selected"
     1208msgstr "%d נבחרו"
     1209
     1210msgid "No subscribers yet."
     1211msgstr "אין עדיין נמענים."
     1212
     1213msgid "No recipients selected. Choose subscribers, or add your own number to send yourself a test."
     1214msgstr "לא נבחרו נמענים. בחרו נמענים, או הוסיפו את המספר שלכם כדי לשלוח לעצמכם בדיקה."
     1215
     1216msgid "%d recipients will receive this campaign."
     1217msgstr "%d נמענים יקבלו את הקמפיין הזה."
     1218
     1219msgid "Send this campaign to %d recipients now?"
     1220msgstr "לשלוח את הקמפיין ל-%d נמענים עכשיו?"
     1221
     1222msgid "Schedule this campaign to %d recipients?"
     1223msgstr "לתזמן את הקמפיין ל-%d נמענים?"
     1224
     1225msgid "Add my number"
     1226msgstr "הוספת המספר שלי"
     1227
     1228msgid "Add to the list"
     1229msgstr "הוספה לרשימה"
     1230
     1231msgid "Adding..."
     1232msgstr "מוסיף..."
     1233
     1234msgid "Added to the list."
     1235msgstr "נוסף לרשימה."
     1236
     1237msgid "Could not add the contact. Please try again."
     1238msgstr "לא ניתן להוסיף את איש הקשר. נסו שוב."
     1239
     1240msgid "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."
     1241msgstr "מוסיף את המספר לרשימת התפוצה כנמען רשום. שימושי לשליחת קמפיין בדיקה לעצמכם: במסך הקמפיינים בחרו \"בחירת אנשי קשר מסוימים\" וסמנו רק את המספר שלכם."
     1242
     1243msgid "Tip: to test on yourself, add your own number on the Marketing List screen, then select only it here."
     1244msgstr "טיפ: לבדיקה על עצמכם, הוסיפו את המספר שלכם במסך רשימת התפוצה ואז סמנו כאן רק אותו."
     1245
     1246msgid "No recipients selected. Choose contacts from the list."
     1247msgstr "לא נבחרו נמענים. בחרו אנשי קשר מהרשימה."
     1248
     1249msgid "No name"
     1250msgstr "ללא שם"
     1251
     1252msgid "Today"
     1253msgstr "היום"
     1254
     1255msgid "Your GoUltra account is currently on hold, so messages cannot be sent. Please contact GoUltra support to resume."
     1256msgstr "חשבון ה-GoUltra שלכם מושהה כרגע, ולכן לא ניתן לשלוח הודעות. פנו לתמיכת GoUltra כדי לחדש."
     1257
     1258msgid "Edit name"
     1259msgstr "עריכת שם"
     1260
     1261msgid "Schedule campaign"
     1262msgstr "תזמון הקמפיין"
     1263
     1264msgid "Will be sent on %1$s at %2$s (your store time)."
     1265msgstr "יישלח בתאריך %1$s בשעה %2$s (לפי שעון החנות)."
     1266
     1267msgid "Could not update the name. Please try again."
     1268msgstr "לא ניתן לעדכן את השם. נסו שוב."
     1269
     1270msgid "GoUltra returned HTTP %d."
     1271msgstr "GoUltra החזירה HTTP %d."
     1272
     1273msgid "Template name"
     1274msgstr "שם התבנית"
     1275
     1276msgid "Lowercase letters, numbers and underscores only."
     1277msgstr "אותיות קטנות באנגלית, ספרות וקו תחתון בלבד."
     1278
     1279msgid "Marketing (offers, promotions)"
     1280msgstr "שיווקי (מבצעים, הצעות)"
     1281
     1282msgid "Utility (order, account updates)"
     1283msgstr "שירותי (הזמנה, עדכוני חשבון)"
     1284
     1285msgid "No header"
     1286msgstr "ללא כותרת"
     1287
     1288msgid "Text"
     1289msgstr "טקסט"
     1290
     1291msgid "Video"
     1292msgstr "וידאו"
     1293
     1294msgid "Document"
     1295msgstr "מסמך"
     1296
     1297msgid "Header text"
     1298msgstr "טקסט הכותרת"
     1299
     1300msgid "You will choose the actual image/video/file when you send a campaign."
     1301msgstr "תבחרו את התמונה/וידאו/קובץ בפועל בעת שליחת קמפיין."
     1302
     1303msgid "Note:"
     1304msgstr "לתשומת לב:"
     1305
     1306msgid "Utility templates are for transactional messages only (order, shipping, account). Promotional content will be rejected by Meta."
     1307msgstr "תבניות שירותיות מיועדות להודעות טרנזקציוניות בלבד (הזמנה, משלוח, חשבון). תוכן פרסומי יידחה על ידי Meta."
     1308
     1309msgid "Insert a variable"
     1310msgstr "הוספת משתנה"
     1311
     1312msgid "(filled in automatically at send time)"
     1313msgstr "(מתמלא אוטומטית בעת השליחה)"
     1314
     1315msgid "Buttons"
     1316msgstr "כפתורים"
     1317
     1318msgid "(up to 10, optional)"
     1319msgstr "(עד 10, אופציונלי)"
     1320
     1321msgid "Add button"
     1322msgstr "הוספת כפתור"
     1323
     1324msgid "Opt-out method (required for marketing)"
     1325msgstr "שיטת הסרה (חובה לשיווקי)"
     1326
     1327msgid "Footer text (recommended)"
     1328msgstr "טקסט תחתון (מומלץ)"
     1329
     1330msgid "Unsubscribe button"
     1331msgstr "כפתור הסרה"
     1332
     1333msgid "Footer"
     1334msgstr "טקסט תחתון"
     1335
     1336msgid "(small gray line, optional)"
     1337msgstr "(שורה אפורה קטנה, אופציונלי)"
     1338
     1339msgid "Quick reply"
     1340msgstr "תגובה מהירה"
     1341
     1342msgid "URL link"
     1343msgstr "קישור URL"
     1344
     1345msgid "Phone number"
     1346msgstr "מספר טלפון"
     1347
     1348msgid "Button text"
     1349msgstr "טקסט הכפתור"
     1350
     1351msgid "A URL or phone button needs a value."
     1352msgstr "כפתור URL או טלפון דורש ערך."
     1353
     1354msgid "Remove"
     1355msgstr "הסרה"
     1356
     1357msgid "Two variables cannot be next to each other. Add some text between them (Meta rejects it otherwise)."
     1358msgstr "שני משתנים לא יכולים להיות זה לצד זה. הוסיפו טקסט ביניהם (אחרת Meta תדחה)."
     1359
     1360msgid "The message text must be between 5 and 1024 characters."
     1361msgstr "טקסט ההודעה חייב להיות בין 5 ל-1024 תווים."
     1362
     1363msgid "Meta rejected the template. %1$s (fallback: %2$s)"
     1364msgstr "Meta דחתה את התבנית. %1$s (גיבוי: %2$s)"
     1365
     1366msgid "Unknown error."
     1367msgstr "שגיאה לא ידועה."
     1368
     1369msgid "The template could not be submitted. %s"
     1370msgstr "לא ניתן היה להגיש את התבנית. %s"
     1371
     1372msgid "This number is not on your marketing list, so there is no name to update."
     1373msgstr "המספר הזה אינו ברשימת השיווק שלכם, ולכן אין שם לעדכן."
     1374
     1375msgid "Paste your API key (it starts with gk_) from the GoUltra Dashboard → Settings → API."
     1376msgstr "הדביקו את מפתח ה-API (מתחיל ב-gk_) מלוח GoUltra → הגדרות → API."
     1377
     1378msgid "Campaign name (optional)"
     1379msgstr "שם הקמפיין (אופציונלי)"
     1380
     1381msgid "e.g. Summer Sale - July"
     1382msgstr "לדוגמה: מבצע קיץ - יולי"
     1383
     1384msgid "A name to recognize this campaign in your history."
     1385msgstr "שם שיעזור לזהות את הקמפיין בהיסטוריה."
     1386
     1387msgid "Upload / Media Library"
     1388msgstr "העלאה / ספריית המדיה"
     1389
     1390msgid "Image selected"
     1391msgstr "תמונה נבחרה"
     1392
     1393msgid "Or paste an image URL"
     1394msgstr "או הדביקו כתובת URL של תמונה"
     1395
     1396msgid "JPEG or PNG images only. Choose from your Media Library, pick a store product, or paste a URL."
     1397msgstr "תמונות JPEG או PNG בלבד. בחרו מספריית המדיה, בחרו מוצר מהחנות, או הדביקו כתובת URL."
     1398
     1399msgid "Enter a valid WhatsApp number, including the country code."
     1400msgstr "הזינו מספר WhatsApp תקין, כולל קידומת מדינה."
     1401
     1402msgid "Too many code requests. Please wait a while before trying again."
     1403msgstr "יותר מדי בקשות קוד. המתינו מעט ונסו שוב."
     1404
     1405msgid "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."
     1406msgstr "תבנית האימות שלכם ב-WhatsApp עדיין בתהליך אישור אצל Meta (בדרך כלל כמה דקות, לעיתים יותר). נסו שוב עוד מעט - לא נדרש מכם דבר נוסף."
     1407
     1408msgid "Could not send the code. Please check the number and try again."
     1409msgstr "לא ניתן היה לשלוח את הקוד. בדקו את המספר ונסו שוב."
     1410
     1411msgid "We sent a 6-digit code to %s on WhatsApp. Enter it below."
     1412msgstr "שלחנו קוד בן 6 ספרות אל %s ב-WhatsApp. הזינו אותו למטה."
     1413
     1414msgid "The code expired. Please request a new one."
     1415msgstr "הקוד פג. בקשו קוד חדש."
     1416
     1417msgid "Too many wrong attempts. Please request a new code."
     1418msgstr "יותר מדי ניסיונות שגויים. בקשו קוד חדש."
     1419
     1420msgid "Incorrect code. Attempts left: %d."
     1421msgstr "קוד שגוי. ניסיונות שנותרו: %d."
     1422
     1423msgid "Your number is verified. It is now your store manager number and can be selected as a campaign recipient for your own tests."
     1424msgstr "המספר שלכם אומת. הוא כעת מספר מנהל החנות שלכם, וניתן לבחור אותו כנמען בקמפיין לבדיקות שלכם."
     1425
     1426msgid "Store manager"
     1427msgstr "מנהל החנות"
     1428
     1429msgid "Verify your number (recommended)"
     1430msgstr "אמתו את המספר שלכם (מומלץ)"
     1431
     1432msgid "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."
     1433msgstr "אשרו את מספר ה-WhatsApp שלכם באמצעות קוד. מספר מאומת הופך למספר מנהל החנות, שאותו תוכלו לבחור כנמען בקמפיין כדי לבדוק את הקמפיינים שלכם."
     1434
     1435msgid "Verified"
     1436msgstr "מאומת"
     1437
     1438msgid "Verify a different number"
     1439msgstr "אימות מספר אחר"
     1440
     1441msgid "Send code"
     1442msgstr "שליחת קוד"
     1443
     1444msgid "Verify"
     1445msgstr "אימות"
     1446
     1447msgid "Resend code"
     1448msgstr "שליחה חוזרת של הקוד"
     1449
     1450msgid "Verifying..."
     1451msgstr "מאמת..."
     1452
     1453msgid "Enter your WhatsApp number first."
     1454msgstr "הזינו קודם את מספר ה-WhatsApp שלכם."
     1455
     1456msgid "Enter the code you received."
     1457msgstr "הזינו את הקוד שקיבלתם."
     1458
     1459msgid "Could not send the code."
     1460msgstr "לא ניתן היה לשלוח את הקוד."
     1461
     1462msgid "Incorrect code."
     1463msgstr "קוד שגוי."
     1464
     1465msgid "Network error. Please try again."
     1466msgstr "שגיאת רשת. נסו שוב."
     1467
     1468msgid "Building a template for a product?"
     1469msgstr "בונים תבנית עבור מוצר?"
     1470
     1471msgid "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."
     1472msgstr "בחרו מוצר מהחנות ונכין עבורו את התבנית: כותרת תמונה וכפתור \"צפייה במוצר\" שמקשר אליו. אחר כך בחרו שפה ושלחו לאישור."
     1473
     1474msgid "View product"
     1475msgstr "צפייה במוצר"
     1476
     1477msgid "No products found."
     1478msgstr "לא נמצאו מוצרים."
     1479
     1480msgid "You already have an approved or pending template of this type for this language. Create another one?"
     1481msgstr "כבר יש לכם תבנית מאושרת או ממתינה מסוג זה בשפה הזאת. ליצור עוד אחת?"
     1482
     1483msgid "Unconfirmed - verify before resending"
     1484msgstr "לא אושר - לבדוק לפני שליחה חוזרת"
     1485
     1486msgid "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."
     1487msgstr "החיבור נותק לפני שקיבלנו תשובה, כך שייתכן שהקמפיין נשלח וייתכן שלא. בדקו בהיסטוריית הקמפיינים ובלוח הבקרה של GoUltra לפני שליחה חוזרת, כדי לא לשלוח פעמיים."
     1488
     1489msgid "GoUltra for WooCommerce: connection problem."
     1490msgstr "GoUltra ל-WooCommerce: בעיית חיבור."
     1491
     1492msgid "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."
     1493msgstr "הודעות WhatsApp אחרונות נדחו כי מפתח ה-API כבר לא תקף (ייתכן שנוצר מחדש או שהחשבון השתנה). הודעות אינן נשלחות."
     1494
     1495msgid "Reconnect your store"
     1496msgstr "חברו מחדש את החנות"
     1497
     1498msgid "Image"
     1499msgstr "תמונה"
     1500
     1501msgid "View"
     1502msgstr "צפייה"
     1503
     1504msgid "rejected"
     1505msgstr "נדחתה"
     1506
     1507msgid "No templates match your search."
     1508msgstr "אין תבניות שתואמות את החיפוש."
     1509
     1510msgid "Search your templates by name, text or language..."
     1511msgstr "חיפוש בתבניות שלך לפי שם, טקסט או שפה..."
     1512
     1513msgid "Close"
     1514msgstr "סגירה"
     1515
     1516msgid "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."
     1517msgstr "GoUltra לא קיבלה את המפתח של החנות עבור פעולה זו. החיבור שלכם תקין (הודעות הזמנה נשלחות), אבל ייתכן ששליחת קמפיינים למספר הזה עדיין לא הופעלה בחשבון GoUltra שלכם. אנא פנו לתמיכת GoUltra כדי להפעיל אותה."
     1518
     1519msgid "JPEG or PNG images only."
     1520msgstr "תמונות JPEG או PNG בלבד."
     1521
     1522msgid "Marketing sending is turned off for this WhatsApp number. Enable marketing for this number in your GoUltra dashboard to send campaigns."
     1523msgstr "שליחת הודעות שיווקיות כבויה עבור מספר ה-WhatsApp הזה. הפעילו שיווק למספר הזה בלוח הבקרה של GoUltra כדי לשלוח קמפיינים."
     1524
     1525msgid "Chat with us"
     1526msgstr "צ'אט איתנו"
     1527
     1528msgid "Chat on WhatsApp"
     1529msgstr "צ'אט בוואטסאפ"
     1530
     1531msgid "about %s"
     1532msgstr "לגבי %s"
     1533
     1534msgid "Ask about this product on WhatsApp"
     1535msgstr "שאלו על המוצר הזה בוואטסאפ"
     1536
     1537msgid "WhatsApp Chat Button"
     1538msgstr "כפתור צ'אט וואטסאפ"
     1539
     1540msgid "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."
     1541msgstr "הוסיפו לחנות כפתור צ'אט וואטסאפ חינמי בלחיצה אחת. הוא עובד ללא חשבון GoUltra: הוא פותח צ'אט וואטסאפ למספר שלכם, באמצעות קישור צ'אט רגיל."
     1542
     1543msgid "Show the floating chat button"
     1544msgstr "הצג את כפתור הצ'אט הצף"
     1545
     1546msgid "A round WhatsApp button in the corner of your storefront."
     1547msgstr "כפתור וואטסאפ עגול בפינת החנות שלכם."
     1548
     1549msgid "Include the country code. This is the number that will receive the chats."
     1550msgstr "כללו את קידומת המדינה. זהו המספר שיקבל את הצ'אטים."
     1551
     1552msgid "Pre-filled message"
     1553msgstr "הודעה מוכנה מראש"
     1554
     1555msgid "Hi! I have a question about your store."
     1556msgstr "שלום! יש לי שאלה על החנות שלכם."
     1557
     1558msgid "The message the customer starts with. Leave empty for none."
     1559msgstr "ההודעה שבה הלקוח מתחיל. השאירו ריק כדי לא להגדיר."
     1560
     1561msgid "Button label"
     1562msgstr "תווית הכפתור"
     1563
     1564msgid "Position"
     1565msgstr "מיקום"
     1566
     1567msgid "Bottom right"
     1568msgstr "למטה מימין"
     1569
     1570msgid "Bottom left"
     1571msgstr "למטה משמאל"
     1572
     1573msgid "Show on"
     1574msgstr "הצג ב"
     1575
     1576msgid "All pages"
     1577msgstr "כל העמודים"
     1578
     1579msgid "Shop and product pages only"
     1580msgstr "עמודי חנות ומוצר בלבד"
     1581
     1582msgid "Add a button on product pages"
     1583msgstr "הוסיפו כפתור בעמודי מוצר"
     1584
     1585msgid "Shows an \"Ask about this product\" WhatsApp button under Add to cart, with the product name filled in."
     1586msgstr "מציג כפתור וואטסאפ \"שאלו על המוצר הזה\" מתחת ללחצן הוספה לעגלה, עם שם המוצר."
     1587
     1588msgid "Tip: you can also place a button anywhere with the shortcode"
     1589msgstr "טיפ: תוכלו למקם כפתור בכל מקום בעזרת השורטקוד"
  • goultra-for-woocommerce/trunk/languages/goultra-for-woocommerce.pot

    r3603449 r3617733  
    12301230msgid "Send a test message (optional)"
    12311231msgstr ""
     1232
     1233msgid "Campaigns"
     1234msgstr ""
     1235
     1236msgid "New campaign"
     1237msgstr ""
     1238
     1239msgid "Compose a WhatsApp campaign for your Marketing List subscribers. Unsubscribed contacts are always filtered out automatically."
     1240msgstr ""
     1241
     1242msgid "Connect your store to GoUltra to send campaigns."
     1243msgstr ""
     1244
     1245msgid "Template"
     1246msgstr ""
     1247
     1248msgid "Refresh"
     1249msgstr ""
     1250
     1251msgid "No approved marketing templates yet. Create one below."
     1252msgstr ""
     1253
     1254msgid "Variables"
     1255msgstr ""
     1256
     1257msgid "Header image"
     1258msgstr ""
     1259
     1260msgid "Media Library"
     1261msgstr ""
     1262
     1263msgid "Pick a product"
     1264msgstr ""
     1265
     1266msgid "Search products"
     1267msgstr ""
     1268
     1269msgid "Send a test to your phone first"
     1270msgstr ""
     1271
     1272msgid "Send test"
     1273msgstr ""
     1274
     1275msgid "Test sent. Check your WhatsApp."
     1276msgstr ""
     1277
     1278msgid "Audience"
     1279msgstr ""
     1280
     1281msgid "When to send"
     1282msgstr ""
     1283
     1284msgid "Send now"
     1285msgstr ""
     1286
     1287msgid "Schedule for later"
     1288msgstr ""
     1289
     1290msgid "Launch campaign"
     1291msgstr ""
     1292
     1293msgid "online"
     1294msgstr ""
     1295
     1296msgid "Choose a template to see the preview."
     1297msgstr ""
     1298
     1299msgid "Campaign history"
     1300msgstr ""
     1301
     1302msgid "Sent"
     1303msgstr ""
     1304
     1305msgid "Failed"
     1306msgstr ""
     1307
     1308msgid "Create a marketing template"
     1309msgstr ""
     1310
     1311msgid "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."
     1312msgstr ""
     1313
     1314msgid "Language"
     1315msgstr ""
     1316
     1317msgid "Style"
     1318msgstr ""
     1319
     1320msgid "Message text"
     1321msgstr ""
     1322
     1323msgid "Button label (optional)"
     1324msgstr ""
     1325
     1326msgid "Button link (optional)"
     1327msgstr ""
     1328
     1329msgid "Submit for approval"
     1330msgstr ""
     1331
     1332msgid "Something went wrong. Please try again."
     1333msgstr ""
     1334
     1335msgid "Choose a template"
     1336msgstr ""
     1337
     1338msgid "Choose a template first."
     1339msgstr ""
     1340
     1341msgid "Awaiting Meta approval:"
     1342msgstr ""
     1343
     1344msgid "Customer first name"
     1345msgstr ""
     1346
     1347msgid "Customer full name"
     1348msgstr ""
     1349
     1350msgid "Fixed value"
     1351msgstr ""
     1352
     1353msgid "No products with a usable image were found."
     1354msgstr ""
     1355
     1356msgid "Sending..."
     1357msgstr ""
     1358
     1359msgid "Please fill in all template variables."
     1360msgstr ""
     1361
     1362msgid "This template needs a header image. Choose one from the media library, a product, or paste an https image link."
     1363msgstr ""
     1364
     1365msgid "%d subscribers will receive this campaign."
     1366msgstr ""
     1367
     1368msgid "Send this campaign to %d subscribers now?"
     1369msgstr ""
     1370
     1371msgid "Schedule this campaign to %d subscribers?"
     1372msgstr ""
     1373
     1374msgid "Campaign launched. Messages are on their way."
     1375msgstr ""
     1376
     1377msgid "Campaign scheduled."
     1378msgstr ""
     1379
     1380msgid "No campaigns yet."
     1381msgstr ""
     1382
     1383msgid "Pause"
     1384msgstr ""
     1385
     1386msgid "Cancel"
     1387msgstr ""
     1388
     1389msgid "Pause this campaign?"
     1390msgstr ""
     1391
     1392msgid "Cancel this scheduled campaign?"
     1393msgstr ""
     1394
     1395msgid "Processing"
     1396msgstr ""
     1397
     1398msgid "Scheduled"
     1399msgstr ""
     1400
     1401msgid "Completed"
     1402msgstr ""
     1403
     1404msgid "Cancelled"
     1405msgstr ""
     1406
     1407msgid "Paused"
     1408msgstr ""
     1409
     1410msgid "Opt-out footer (added automatically):"
     1411msgstr ""
     1412
     1413msgid "The message text must be between 15 and 1024 characters."
     1414msgstr ""
     1415
     1416msgid "Template submitted. Meta approval usually takes minutes to a few hours."
     1417msgstr ""
     1418
     1419msgid "A button needs both a label and a link."
     1420msgstr ""
     1421
     1422msgid "Could not submit the template. Please try again."
     1423msgstr ""
     1424
     1425msgid "Could not load templates. Please try again."
     1426msgstr ""
     1427
     1428msgid "Test send failed. Please try again."
     1429msgstr ""
     1430
     1431msgid "Could not launch the campaign. Please try again."
     1432msgstr ""
     1433
     1434msgid "The action failed. Please try again."
     1435msgstr ""
     1436
     1437msgid "Invalid request."
     1438msgstr ""
     1439
     1440msgid "Choose a valid date and time for the scheduled send."
     1441msgstr ""
     1442
     1443msgid "A campaign was just launched. Please wait two minutes before launching another."
     1444msgstr ""
     1445
     1446msgid "Your audience is larger than 5,000 contacts. Please run this campaign from the GoUltra dashboard."
     1447msgstr ""
     1448
     1449msgid "Your GoUltra account has more than one WhatsApp number connected, so the sending number must be chosen in the GoUltra dashboard for this store."
     1450msgstr ""
     1451
     1452msgid "Your GoUltra plan message allowance for this cycle has been reached. Upgrade your plan to send more."
     1453msgstr ""
     1454
     1455msgid "GoUltra for WooCommerce - WhatsApp Order Notifications & Abandoned Cart"
     1456msgstr ""
     1457
     1458msgid "Automate WooCommerce WhatsApp order updates, shipping notifications, abandoned cart recovery and opt-in marketing through GoUltra."
     1459msgstr ""
     1460
     1461msgid "Marketing Offer"
     1462msgstr ""
     1463
     1464msgid "Promotions and offers for opted-in subscribers"
     1465msgstr ""
     1466
     1467msgid "Recipients"
     1468msgstr ""
     1469
     1470msgid "All subscribers"
     1471msgstr ""
     1472
     1473msgid "Choose specific contacts"
     1474msgstr ""
     1475
     1476msgid "Search name or number"
     1477msgstr ""
     1478
     1479msgid "Select all"
     1480msgstr ""
     1481
     1482msgid "Clear"
     1483msgstr ""
     1484
     1485msgid "Also send to my own number (%s) for a test"
     1486msgstr ""
     1487
     1488msgid "Also send to my own number for a test (set your admin number in Settings)"
     1489msgstr ""
     1490
     1491msgid "Need a marketing template?"
     1492msgstr ""
     1493
     1494msgid "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."
     1495msgstr ""
     1496
     1497msgid "Open Templates"
     1498msgstr ""
     1499
     1500msgid "Delivery receipts and product sync"
     1501msgstr ""
     1502
     1503msgid "These work automatically once your store is connected. There is nothing to set up."
     1504msgstr ""
     1505
     1506msgid "Connected and active."
     1507msgstr ""
     1508
     1509msgid "Message delivery receipts appear as order notes, and your product catalog is available to GoUltra automatically. No secret or URL to copy anywhere."
     1510msgstr ""
     1511
     1512msgid "Technical details (for support)"
     1513msgstr ""
     1514
     1515msgid "Webhook secret"
     1516msgstr ""
     1517
     1518msgid "The plugin shares the secret with GoUltra automatically on connect, so you never paste it anywhere."
     1519msgstr ""
     1520
     1521msgid "All subscribers (%d)"
     1522msgstr ""
     1523
     1524msgid "%d selected"
     1525msgstr ""
     1526
     1527msgid "No subscribers yet."
     1528msgstr ""
     1529
     1530msgid "No recipients selected. Choose subscribers, or add your own number to send yourself a test."
     1531msgstr ""
     1532
     1533msgid "%d recipients will receive this campaign."
     1534msgstr ""
     1535
     1536msgid "Send this campaign to %d recipients now?"
     1537msgstr ""
     1538
     1539msgid "Schedule this campaign to %d recipients?"
     1540msgstr ""
     1541
     1542msgid "Add my number"
     1543msgstr ""
     1544
     1545msgid "Add to the list"
     1546msgstr ""
     1547
     1548msgid "Adding..."
     1549msgstr ""
     1550
     1551msgid "Added to the list."
     1552msgstr ""
     1553
     1554msgid "Could not add the contact. Please try again."
     1555msgstr ""
     1556
     1557msgid "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."
     1558msgstr ""
     1559
     1560msgid "Tip: to test on yourself, add your own number on the Marketing List screen, then select only it here."
     1561msgstr ""
     1562
     1563msgid "No recipients selected. Choose contacts from the list."
     1564msgstr ""
     1565
     1566msgid "No name"
     1567msgstr ""
     1568
     1569msgid "Today"
     1570msgstr ""
     1571
     1572msgid "Your GoUltra account is currently on hold, so messages cannot be sent. Please contact GoUltra support to resume."
     1573msgstr ""
     1574
     1575msgid "Edit name"
     1576msgstr ""
     1577
     1578msgid "Schedule campaign"
     1579msgstr ""
     1580
     1581msgid "Will be sent on %1$s at %2$s (your store time)."
     1582msgstr ""
     1583
     1584msgid "Could not update the name. Please try again."
     1585msgstr ""
     1586
     1587msgid "GoUltra returned HTTP %d."
     1588msgstr ""
     1589
     1590msgid "Template name"
     1591msgstr ""
     1592
     1593msgid "Lowercase letters, numbers and underscores only."
     1594msgstr ""
     1595
     1596msgid "Marketing (offers, promotions)"
     1597msgstr ""
     1598
     1599msgid "Utility (order, account updates)"
     1600msgstr ""
     1601
     1602msgid "No header"
     1603msgstr ""
     1604
     1605msgid "Text"
     1606msgstr ""
     1607
     1608msgid "Video"
     1609msgstr ""
     1610
     1611msgid "Document"
     1612msgstr ""
     1613
     1614msgid "Header text"
     1615msgstr ""
     1616
     1617msgid "You will choose the actual image/video/file when you send a campaign."
     1618msgstr ""
     1619
     1620msgid "Note:"
     1621msgstr ""
     1622
     1623msgid "Utility templates are for transactional messages only (order, shipping, account). Promotional content will be rejected by Meta."
     1624msgstr ""
     1625
     1626msgid "Insert a variable"
     1627msgstr ""
     1628
     1629msgid "(filled in automatically at send time)"
     1630msgstr ""
     1631
     1632msgid "Buttons"
     1633msgstr ""
     1634
     1635msgid "(up to 10, optional)"
     1636msgstr ""
     1637
     1638msgid "Add button"
     1639msgstr ""
     1640
     1641msgid "Opt-out method (required for marketing)"
     1642msgstr ""
     1643
     1644msgid "Footer text (recommended)"
     1645msgstr ""
     1646
     1647msgid "Unsubscribe button"
     1648msgstr ""
     1649
     1650msgid "Footer"
     1651msgstr ""
     1652
     1653msgid "(small gray line, optional)"
     1654msgstr ""
     1655
     1656msgid "Quick reply"
     1657msgstr ""
     1658
     1659msgid "URL link"
     1660msgstr ""
     1661
     1662msgid "Phone number"
     1663msgstr ""
     1664
     1665msgid "Button text"
     1666msgstr ""
     1667
     1668msgid "A URL or phone button needs a value."
     1669msgstr ""
     1670
     1671msgid "Remove"
     1672msgstr ""
     1673
     1674msgid "Two variables cannot be next to each other. Add some text between them (Meta rejects it otherwise)."
     1675msgstr ""
     1676
     1677msgid "The message text must be between 5 and 1024 characters."
     1678msgstr ""
     1679
     1680msgid "Meta rejected the template. %1$s (fallback: %2$s)"
     1681msgstr ""
     1682
     1683msgid "Unknown error."
     1684msgstr ""
     1685
     1686msgid "The template could not be submitted. %s"
     1687msgstr ""
     1688
     1689msgid "This number is not on your marketing list, so there is no name to update."
     1690msgstr ""
     1691
     1692msgid "Paste your API key (it starts with gk_) from the GoUltra Dashboard → Settings → API."
     1693msgstr ""
     1694
     1695msgid "Campaign name (optional)"
     1696msgstr ""
     1697
     1698msgid "e.g. Summer Sale - July"
     1699msgstr ""
     1700
     1701msgid "A name to recognize this campaign in your history."
     1702msgstr ""
     1703
     1704msgid "Upload / Media Library"
     1705msgstr ""
     1706
     1707msgid "Image selected"
     1708msgstr ""
     1709
     1710msgid "Or paste an image URL"
     1711msgstr ""
     1712
     1713msgid "JPEG or PNG images only. Choose from your Media Library, pick a store product, or paste a URL."
     1714msgstr ""
     1715
     1716msgid "Enter a valid WhatsApp number, including the country code."
     1717msgstr ""
     1718
     1719msgid "Too many code requests. Please wait a while before trying again."
     1720msgstr ""
     1721
     1722msgid "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."
     1723msgstr ""
     1724
     1725msgid "Could not send the code. Please check the number and try again."
     1726msgstr ""
     1727
     1728msgid "We sent a 6-digit code to %s on WhatsApp. Enter it below."
     1729msgstr ""
     1730
     1731msgid "The code expired. Please request a new one."
     1732msgstr ""
     1733
     1734msgid "Too many wrong attempts. Please request a new code."
     1735msgstr ""
     1736
     1737msgid "Incorrect code. Attempts left: %d."
     1738msgstr ""
     1739
     1740msgid "Your number is verified. It is now your store manager number and can be selected as a campaign recipient for your own tests."
     1741msgstr ""
     1742
     1743msgid "Store manager"
     1744msgstr ""
     1745
     1746msgid "Verify your number (recommended)"
     1747msgstr ""
     1748
     1749msgid "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."
     1750msgstr ""
     1751
     1752msgid "Verified"
     1753msgstr ""
     1754
     1755msgid "Verify a different number"
     1756msgstr ""
     1757
     1758msgid "Send code"
     1759msgstr ""
     1760
     1761msgid "Verify"
     1762msgstr ""
     1763
     1764msgid "Resend code"
     1765msgstr ""
     1766
     1767msgid "Verifying..."
     1768msgstr ""
     1769
     1770msgid "Enter your WhatsApp number first."
     1771msgstr ""
     1772
     1773msgid "Enter the code you received."
     1774msgstr ""
     1775
     1776msgid "Could not send the code."
     1777msgstr ""
     1778
     1779msgid "Incorrect code."
     1780msgstr ""
     1781
     1782msgid "Network error. Please try again."
     1783msgstr ""
     1784
     1785msgid "Building a template for a product?"
     1786msgstr ""
     1787
     1788msgid "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."
     1789msgstr ""
     1790
     1791msgid "View product"
     1792msgstr ""
     1793
     1794msgid "No products found."
     1795msgstr ""
     1796
     1797msgid "You already have an approved or pending template of this type for this language. Create another one?"
     1798msgstr ""
  • goultra-for-woocommerce/trunk/readme.txt

    r3603449 r3617733  
    1 === GoUltra for WooCommerce ===
     1=== GoUltra for WooCommerce - WhatsApp Marketing, Notifications & Abandoned Cart ===
    22Contributors: goultraai
    3 Tags: woocommerce, whatsapp, abandoned cart, order notifications, shipping updates
     3Tags: woocommerce-whatsapp, whatsapp-marketing, whatsapp-notification, abandoned-cart, whatsapp-api
    44Requires at least: 6.0
    55Tested up to: 7.0
    66Requires PHP: 7.4
    7 Stable tag: 3.8.2
     7Stable tag: 3.9.24
    88License: GPLv2 or later
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html
    … …  
    1111WC tested up to: 10.8
    1212
    13 Send WhatsApp order confirmations, shipping updates, and abandoned cart recovery messages to your WooCommerce customers via GoUltra.
     13WooCommerce WhatsApp notifications: automated order, shipping and abandoned-cart messages, plus opt-in marketing campaigns, powered by GoUltra.
    1414
    1515== Description ==
    1616
    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.
    1818
    19 = Features =
     19Build 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.
    2020
    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 =
    3022
    31 = How It Works =
     23Create 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.
    3224
    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 =
    3826
    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.
    4036
    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
     45The 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
     49This 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.
    4250
    4351* [GoUltra Terms of Service](https://goultra.ai/terms)
    4452* [GoUltra Privacy Policy](https://goultra.ai/privacy)
    4553
    46 WhatsApp message delivery costs are billed by Meta (WhatsApp) based on conversation type and volume.
     54Your store remains responsible for its consent text, privacy notices, audience eligibility and applicable legal requirements.
    4755
    4856== Installation ==
    4957
    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 don't have one.
     581. Upload the `goultra-for-woocommerce` folder to `/wp-content/plugins/`, or install it from the Plugins screen in WordPress.
     592. Activate the plugin through the **Plugins** menu (WooCommerce must be installed and active).
     603. Go to **GoUltra -> Settings** in the admin sidebar.
     614. Create a GoUltra account at [goultra.ai/register](https://goultra.ai/register) if you do not have one.
    54625. Paste your API key and click **Connect**.
    55 6. Configure your notification preferences and you're done!
     636. Choose which notifications to enable and which approved templates to use.
    5664
    5765== Frequently Asked Questions ==
    … …  
    5967= Do I need a GoUltra account? =
    6068
    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).
     69Yes, 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
     73Yes. 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
     77Yes. 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
     81Yes. 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
     85No. 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
     89Yes. 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.
    6290
    6391= How much does it cost? =
    6492
    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.
     93The 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.
    6694
    67 = How does it handle GDPR and consent? =
     95= Can I send marketing messages to every WooCommerce customer? =
    6896
    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.
     97No. 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
     101No. 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
     105Marketing 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.
    70106
    71107= Does this work with WooCommerce HPOS? =
    72108
    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.
     109Yes. The plugin is fully compatible with HPOS and with both classic and block-based checkout.
    78110
    79111== Screenshots ==
    80112
    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.
     1131. Compose and schedule a WhatsApp marketing campaign, with a live preview and a test send.
     1142. A searchable marketing audience built from shoppers who actively opted in.
     1153. The template builder: marketing, order, shipping, admin and abandoned-cart message templates.
     1164. Active now: every active WooCommerce WhatsApp workflow in one place.
     1175. Map WooCommerce order statuses to approved WhatsApp templates.
     1186. Configure a three-step abandoned-cart recovery sequence.
     1197. Monitor messaging activity and recovered-cart revenue from WordPress.
     1208. Set up the free WhatsApp chat button, with no GoUltra account needed.
    86121
    87122== Changelog ==
    88123
    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.
     124The three most recent releases are below. Older entries are in changelog.txt.
    94125
    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.
    97130
    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.
    103133
    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.
    211138
    212139== Upgrade Notice ==
    213140
    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 =
     142Adds a free WhatsApp chat button (no account needed) and leads with WhatsApp marketing campaigns. Open GoUltra to set it up.
    216143
    217 = 3.7.5 =
    218 Stability 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 =
     145Sending-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.
    219146
    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 =
     148Build a marketing template for a specific product: pick the product and get an image header and a "View product" button automatically.
    222149
    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 =
     151Verify your admin WhatsApp number with a one-time code, and use that verified store manager number to test your own campaigns.
    225152
    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 =
     154Send 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  
    99
    1010$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.
    1214$connected_phone        = get_option( GOULTRA_WC_OPTION_PREFIX . 'connected_phone', '' );
    1315$business_name          = get_option( GOULTRA_WC_OPTION_PREFIX . 'business_name', '' );
    … …  
    4951        <div><h1>GoUltra for WooCommerce <span class="gu-hdr-ver">v<?php echo esc_html( GOULTRA_WC_VERSION ); ?></span></h1></div>
    5052        <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>
    5253            <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>
    5358            <a href="https://goultra.ai/docs/woocommerce" target="_blank"><?php esc_html_e( 'Docs', 'goultra-for-woocommerce' ); ?></a>
    5459            <a href="https://goultra.ai/support" target="_blank"><?php esc_html_e( 'Support', 'goultra-for-woocommerce' ); ?></a>
    … …  
    8388                    <label><?php esc_html_e( 'API Key', 'goultra-for-woocommerce' ); ?></label>
    8489                    <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" />
    8692                        <button type="button" class="gu-btn gu-btn-p" id="gu-conn-btn"><?php esc_html_e( 'Connect', 'goultra-for-woocommerce' ); ?></button>
    8793                    </div>
    88                     <div class="gu-desc"><?php esc_html_e( 'Paste your API key from GoUltra 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>
    8995                </div>
    9096            <?php endif; ?>
    … …  
    113119        // Legacy fallback (no mapping saved yet): the old toggles govern.
    114120        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' ) ) {
    116122                $gu_live_cust[] = $gu_live_lbl( get_option( GOULTRA_WC_OPTION_PREFIX . 'order_confirm_trigger', 'processing' ) );
    117123            }
    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' ) ) {
    119125                $gu_live_admin[] = __( 'New order', 'goultra-for-woocommerce' );
    120126            }
    121127        }
    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' );
    123129        $gu_live_steps = array();
    124130        foreach ( array( 1, 2, 3 ) as $gu_i ) {
    … …  
    179185
    180186    <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>
    181235
    182236    <!-- ORDER STATUS → WHATSAPP TEMPLATE MAPPING -->
    … …  
    282336                </div>
    283337            </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">&#10003; <?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>
    284368            <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>
    285369            <div id="gu-admin-rows">
    … …  
    312396        <div class="gu-card-b">
    313397            <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>
    315399                <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>
    316400            </div>
    … …  
    478562    <div class="gu-card">
    479563        <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>
    510590        </div>
    511591    </div>
    … …  
    526606            <?php endif; ?>
    527607
    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>
    538608        </div>
    539609        <div class="gu-sf-right">
    … …  
    613683    });
    614684
     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
    615743    $('#gu-settings-form').on('submit', function(e){
    616744        e.preventDefault();
    617745        var $b = $('#gu-save-btn').prop('disabled', true).text('<?php echo esc_js( __( 'Saving...', 'goultra-for-woocommerce' ) ); ?>');
    618746        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){
    620748            if(!$('[name="'+n+'"]').is(':checked')) data += '&' + n + '=no';
    621749        });
  • goultra-for-woocommerce/trunk/templates/admin/subscribers-page.php

    r3603449 r3617733  
    2828        <div class="gu-hdr-links">
    2929            <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>
    3032            <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>
    3136        </div>
    3237    </div>
    … …  
    7176                    <option value="withdrawn"><?php esc_html_e( 'Unsubscribed', 'goultra-for-woocommerce' ); ?></option>
    7277                </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>
    7379                <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>
    7490            </div>
    7591
  • goultra-for-woocommerce/trunk/templates/admin/template-presets.php

    r3588005 r3617733  
    565565        ),
    566566    ),
     567    'marketing_offer' => array(
     568        'en' => array(
     569            array('key'=>"sale",'style'=>"Sale",'tpl'=>"Hi {{first_name}}!
     570
     571Big 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
     577Happy 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
     580It 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
     583Good 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
     622Grandes 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
     634Buenas 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
     639Grande 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
     645Joyeuses 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
     648Il 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
     651Bonne 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
     656Groß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
     662Frohe 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
     665Endlich 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
     668Gute 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
     673Grande 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
     679Buone 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
     682Finalmente è 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
     685Buone 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
     690Grande 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
     696Boas 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
     699Finalmente 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
     702Boa 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
     724Bü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
     730Hepimizden 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
     733Sonunda 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
     741Groot 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
     747Fijne 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
     750Eindelijk 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
     753Goed 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
     764Wesoł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
     767Wreszcie 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
     770Dobra 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
     792Kabar 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
     798Selamat 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
     801Akhirnya 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
     804Kabar 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
     809Berita 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
     815Selamat 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
     818Akhirnya 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
     821Berita 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
     826Tin 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
     832Chú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
     835Cuố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
     838Tin 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    ),
    567926);
  • goultra-for-woocommerce/trunk/templates/admin/templates-page.php

    r3588005 r3617733  
    2222    'abandoned_cart_2'   => __( 'Abandoned Cart - Step 2 (Urgency)', 'goultra-for-woocommerce' ),
    2323    'abandoned_cart_3'   => __( 'Abandoned Cart - Step 3 (Discount)', 'goultra-for-woocommerce' ),
     24    'marketing_offer'    => __( 'Marketing Offer', 'goultra-for-woocommerce' ),
    2425    'custom'             => __( 'Custom Template', 'goultra-for-woocommerce' ),
    2526);
    … …  
    3233    'abandoned_cart_2'   => array( 'icon' => 'cart2',  'bg' => '#fee2e2', 'desc' => __( 'Step 2: Create urgency (4 hours)', 'goultra-for-woocommerce' ) ),
    3334    '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' ) ),
    3436    'custom'             => array( 'icon' => 'custom', 'bg' => '#f1f5f9', 'desc' => __( 'Write your own template with any variables', 'goultra-for-woocommerce' ) ),
    3537);
    … …  
    6264    $saved_by_key[ $tk ][ $t['language'] ] = $st;
    6365
    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).
    6570    if ( ! isset( $all_tpls ) ) $all_tpls = array();
    6671    $all_tpls[ $tk ][ $t['language'] ][] = array(
    … …  
    6873        'style_key'     => $t['style_key'] ?? '',
    6974        'template_name' => $t['template_name'] ?? '',
     75        'body_text'     => ( 'marketing_offer' === $tk ) ? (string) ( $t['body_text'] ?? '' ) : '',
    7076    );
    7177
    … …  
    8086}
    8187
     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, ... ]
     100foreach ( ( $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}
    82105$custom_templates = array();
    83106foreach ( $templates as $t ) {
    84107    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        }
    85128        $custom_templates[] = $t;
    86129    }
    … …  
    127170    '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>',
    128171    '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>',
    129173);
    130174?>
    … …  
    162206            ?><span class="gu-tc-badge" style="background:#f1f5f9;color:#9ca3af;"><?php esc_html_e( 'Not set up', 'goultra-for-woocommerce' ); ?></span><?php
    163207            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>
    165210        </div>
    166211        <?php endforeach; ?>
    … …  
    186231            </div>
    187232        </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" />
    188234        <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;">&times;</button>
     241            <div id="gu-tpl-modal-body"></div>
     242        </div>
    189243    </div>
    190244
    … …  
    212266        <div id="gu-status-banner" style="display:none;margin-bottom:16px;padding:14px 20px;border-radius:12px;font-size:14px;"></div>
    213267        <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>
    232307                            </select>
    233308                        </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>
    239317                            </select>
    240318                        </div>
    241319                    </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>
    250376                        </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>
    279380                        </div>
    280381                    </div>
    … …  
    289390    var GU=window.goUltraAdmin||{};
    290391    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(); } }
    291402    var RTL=['ar','he','fa','ur'];
    292403    var typeMeta=<?php echo wp_json_encode($type_meta); ?>;
    … …  
    307418        approved:<?php echo wp_json_encode( __( 'approved', 'goultra-for-woocommerce' ) ); ?>,
    308419        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' ) ); ?>,
    309423        saving:<?php echo wp_json_encode( __( 'Saving...', 'goultra-for-woocommerce' ) ); ?>,
    310424        submitting:<?php echo wp_json_encode( __( 'Submitting...', 'goultra-for-woocommerce' ) ); ?>,
    … …  
    321435        error:<?php echo wp_json_encode( __( 'Error', 'goultra-for-woocommerce' ) ); ?>,
    322436        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' ) ); ?>
    324448    };
    325449
    … …  
    406530        var rows = (allTpls[selType] && allTpls[selType][selLang]) || [];
    407531        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        };
    409541        for(var ri=0; ri<rows.length; ri++){
    410542            // 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.)
    413546            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            }
    415567        }
    416568
    … …  
    444596    function renderCustomList(){
    445597        var $list=$('#gu-custom-list').empty();
     598        var q=($('#gu-custom-search').val()||'').toLowerCase().trim();
    446599        if(!customTemplates.length){
     600            $('#gu-custom-search').hide();
    447601            $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>');
    448602            return;
    449603        }
     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        }
    450614        var html='<div style="display:grid;gap:10px;">';
    451         customTemplates.forEach(function(t){
     615        rows.forEach(function(t){
     616            var idx=customTemplates.indexOf(t);
    452617            var st=t.status||'draft';
    453618            var badgeBg=st==='approved'?'#dcfce7':st==='pending'?'#fef9c3':st==='rejected'?'#fee2e2':'#f1f5f9';
    454619            var badgeColor=st==='approved'?'#15803d':st==='pending'?'#92400e':st==='rejected'?'#b91c1c':'#475569';
    455620            var badgeIcon=st==='approved'?'✓':st==='pending'?'⏳':st==='rejected'?'✗':'✎';
     621            var stLabel=(st==='approved'?guT.approved:st==='pending'?guT.pending:st==='rejected'?guT.rejected2:st);
    456622            var langLabel=langNames[t.language]||t.language;
    457623            var preview=(t.body_text||'').substring(0,60)+'…';
    … …  
    460626            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>';
    461627            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>';
    463629            html+='<div style="font-size:12px;color:#6b7280;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">'+$('<span>').text(preview).html()+'</div>';
    464630            html+='<div style="font-size:11px;color:#94a3b8;margin-top:2px;">'+$('<span>').text(metaName).html()+'</div>';
    465631            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>';
    467634            html+='</div>';
    468635        });
    … …  
    470637        $list.html(html);
    471638    }
     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;">&#10003;&#10003;</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);
    472670
    473671    // Sync from GoUltra and reload page
    … …  
    484682            // Go to Custom Templates management page
    485683            renderCustomList();
    486             $('#gu-s1').hide();$('#gu-s4').show();
     684            $('#gu-s1').hide();$('#gu-s4').show();guTop();
    487685            return;
    488686        }
    489687        var m=typeMeta[selType]||{};
    490688        $('#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();
    492690    });
    493691
    … …  
    496694        selType='custom';
    497695        $('#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();
    499697    });
    500698
    … …  
    507705        var isRtl=RTL.indexOf(base)!==-1;
    508706
    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){
    555712            var nApproved=0, nPending=0, approvedNames=[], pendingNames=[];
    556713            for(var si=0;si<rows.length;si++){
    … …  
    558715                if(rows[si].status==='pending'){ nPending++; if(rows[si].template_name) pendingNames.push(rows[si].template_name); }
    559716            }
    560 
    561717            var $banner=$('#gu-status-banner').hide().empty();
    562718            if(nApproved > 0 || nPending > 0){
    563719                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>';
    566722                html+='</div>';
    567723                if(approvedNames.length) html+='<div style="margin-top:6px;font-size:11px;color:#6b7280;">'+approvedNames.map(esc).join(', ')+'</div>';
    … …  
    571727                $banner.html(html).css({background:bg,border:'1.5px solid '+border}).show();
    572728            }
     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]) || []);
    573751
    574752            var fk=fontKey(selLang);
    … …  
    583761        }
    584762
    585         $('#gu-s2').hide();$('#gu-s3').show();
     763        $('#gu-s2').hide();$('#gu-s3').show();guTop();
    586764        setTimeout(function(){
    587765            var maxH=0;
    … …  
    591769    });
    592770
    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)+'">&#10005;</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
    595958        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||'&nbsp;')+'</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
    618978    $('#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;}
    623989        var $btn=$(this).prop('disabled',true).text(guT.submitting);
    624         var $msg=$('#gu-custom-msg').hide();
     990        $msg.hide();
    625991        $.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();});
    6521012    $('#gu-b2').on('click',function(){
    6531013        $('#gu-s3').hide();
    … …  
    6601020            $('#gu-s2').show();
    6611021        }
     1022        guTop();
    6621023    });
    6631024    $('#gu-ls').on('input',function(){
    … …  
    6701031        var variants=getVariants(selType,selLang);
    6711032        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);
    6731056        $.post(GU.ajaxUrl,{
    6741057            action:'goultra_save_template',nonce:GU.nonce,
  • goultra-for-woocommerce/trunk/uninstall.php

    r3588005 r3617733  
    2727$wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}goultra_templates" );
    2828$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'" );
    2933
    3034// Remove all transients
    … …  
    4751        'goultra_async_send',
    4852        'goultra_async_shipping',
     53        'goultra_async_consent',
     54        'goultra_async_redact',
    4955    );
    5056    foreach ( $goultra_hooks as $goultra_hook ) {
Note: See TracChangeset for help on using the changeset viewer.