WEB SDK Vanilla Entegrasyonu Örnek Ekran Bileşenleri
Bu doküman, BEX Ödeme WEB SDK’nın Vanilla HTML-JS-CSS bir web uygulamasına entegre ederken kullanılabilecek örnek ekran kodlarını gösterir. Bu kodlar sdk'nın ekran ile kullanımını örneklendirir, kullanımı zorunlu olmamakla beraber, iş yerleri buradaki örnekleri güncelleyerek kullanabilirler. SDK, arkaplan işlemleri ve güvenli kart verisi yönetiminden sorumludur; ekran akışları, UI bileşenleri ve müşteri deneyimi tamamen entegratör kuruma aittir. Örnek ekranlar web-component olarak sunulmuştur, state olarak da window üzerinden global bir state yönetimi kullanılmıştır.
1. Index ekranı
Web Component'lar aşağıdaki şekilde eklenebilir:
<merchant-screen-register id="screen-register" class="screen" aria-label="Yeni kart" hidden></merchant-screen-register>
<merchant-screen-link id="screen-link" class="screen" aria-label="Hesap bağlama" hidden></merchant-screen-link>
<merchant-screen-saved id="screen-saved" class="screen" aria-label="Kayıtlı kartlar" hidden ></merchant-screen-saved>
<merchant-screen-otp id="screen-otp" class="screen" aria-label="OTP" hidden></merchant-screen-otp>
<merchant-screen-payment id="screen-payment" class="screen" aria-label="Ödeme detayı" hidden></merchant-screen-payment>
</div>
<script src="app.js" defer></script>
<script src="web-components/screens/merchant-screen-register.js" defer></script>
<script src="web-components/screens/merchant-screen-link.js" defer></script>
<script src="web-components/screens/merchant-screen-saved.js" defer></script>
<script src="web-components/screens/merchant-screen-otp.js" defer></script>
<script src="web-components/screens/merchant-screen-payment.js" defer></script>
<script src = "sdk.umd.js"></script>
2. Register Ekranı
/**
* Yeni kart ekranı — tek dosya olarak kopyalanabilir.
* Bağımlılık: styles.css (register-card-* sınıfları); MerchantDemo.state; merchant:navigate.
*/
(function () {
"use strict";
function navigate(screen) {
document.dispatchEvent(new CustomEvent("merchant:navigate", { detail: { screen } }));
}
class MerchantScreenRegister extends HTMLElement {
connectedCallback() {
if (this._ready) return;
this._ready = true;
this.classList.add("screen");
this.innerHTML = `
<div class="reg-inner">
<header class="reg-header">
<button type="button" class="icon-btn reg-back" aria-label="Geri">‹</button>
<h1 class="reg-title">Yeni Kart Ekle</h1>
</header>
<hr class="divider" />
<div id = "card-field-register"></div>
<div class="bkm-box">
<div class="bkm-head">BKM Express</div>
<div class="bkm-body">
Kartınızı <strong>BKM Ekspress</strong>'e kaydederek ödemelerinizi hızlı ve güvenli şekilde yapabilirsiniz.
<div class="bkm-row">
<input type="checkbox" id="reg-bkm" />
<label for="reg-bkm">
<button type="button" class="link" data-link="sozlesme">BKM Express Kullanıcı Sözleşmesi</button>'ni okudum ve kabul ediyorum.
Detaylı bilgi için <button type="button" class="link" data-link="aydinlatma">Aydınlatma Metni</button>'ni inceleyiniz.
</label>
</div>
</div>
</div>
</div>
<div class="reg-footer">
<button type="button" class="btn-teal" id="reg-submit">Kaydol</button>
</div>
`;
const q = (sel) => this.querySelector(sel);
const state = window.MerchantDemo && window.MerchantDemo.state;
q(".reg-back").addEventListener("click", () => {
console.log("[register] Geri");
const back = state && state.registerSource === "link" ? "link" : "init";
navigate(back);
});
q("#reg-bkm").addEventListener("change", (ev) => {
console.log("[register] BKM onay kutusu:", ev.target.checked);
});
q(".bkm-body").addEventListener("click", (ev) => {
const btn = ev.target.closest("button[data-link]");
if (!btn) return;
console.log("[register] Link tıklandı:", btn.getAttribute("data-link"));
});
q("#reg-submit").addEventListener("click", async () => {
console.log("saved-new-submit");
let agreements = [];
agreements.push({agreementId: window.MerchantDemo.state.client.pendingAgreements[0].agreementId, status: "ACCEPTED"});
agreements.push({agreementId: window.MerchantDemo.state.client.pendingAgreements[2].agreementId, status: "ACCEPTED"});
const resp = await window.MerchantDemo.state.client.registerSecure("test kartım", agreements);
console.log(resp);
if (resp.data.screen == 'EnterOtpScreen')
navigate("otp");
if (resp.data.screen == 'WalletScreen')
navigate("saved");
});
}
}
customElements.define("merchant-screen-register", MerchantScreenRegister);
})();
3. Linkleme Ekranı
/**
* Hesap bağlama (BKM link) ekranı — tek dosya olarak kopyalanabilir.
* Bağımlılık: styles.css; MerchantDemo.state; merchant:navigate.
*/
(function () {
"use strict";
function navigate(screen) {
document.dispatchEvent(new CustomEvent("merchant:navigate", { detail: { screen } }));
}
class MerchantScreenLink extends HTMLElement {
connectedCallback() {
if (this._ready) return;
this._ready = true;
this.classList.add("screen");
this.innerHTML = `
<div class="shell shell--page">
<header class="page-header">
<button type="button" class="icon-btn link-top-back" aria-label="Geri">‹</button>
<h1 class="page-title">Kredi/Banka Kartı ile Ödeme</h1>
</header>
<hr class="divider" />
<div class="link-card">
<div class="link-card-head">
<div class="bkm-brand">BKM <small>express</small></div>
<button type="button" class="btn-link-account" id="btn-link-account">Hesabını Bağla</button>
</div>
<div class="link-card-body">
<h2>Kayıtlı kartınız bulunuyor.</h2>
<p>E-devlet ödemelerinizde hızlı ve güvenli işlem yapmak için BKM Ekspress'e kayıtlı kartlarınızı kullanmak ister misiniz?</p>
<label class="field-label" for="link-phone">Telefon Numarası</label>
<div id="link-phone" class="input input--rounded" type="tel"></div>
</div>
</div>
<p class="or">ya da</p>
<button type="button" class="btn-outline-purple" id="btn-other-card">+ Başka Kart ile Öde</button>
</div>
`;
const q = (sel) => this.querySelector(sel);
const state = window.MerchantDemo && window.MerchantDemo.state;
q(".link-top-back").addEventListener("click", () => {
console.log("[linkleme] Üst geri");
navigate("init");
});
q("#link-phone").addEventListener("input", (ev) => {
console.log("[linkleme] Telefon:", ev.target.value);
});
q("#btn-link-account").addEventListener("click", async () => {
console.log("[linkleme] Hesabını Bağla — telefon:", q("#link-phone").value);
const client = window.MerchantDemo?.state?.client;
const linkWalletResponse = await client.linkWalletToMerchant();
if(linkWalletResponse.data.screen == "EnterOtpScreen") navigate("otp");
else if(linkWalletResponse.data.screen == "WalletScreen") {
const checkWalletResponse = await client.checkWallet();
window.MerchantDemo.state.cardList = checkWalletResponse.data.cards;
const savedScreen = document.getElementById('screen-saved');
savedScreen.cards = window.MerchantDemo.state.cardList; // <-- property set
console.log("saved screen");
navigate("saved");
}
});
q("#btn-other-card").addEventListener("click", () => {
console.log("[linkleme] Başka Kart ile Öde");
if (state) state.registerSource = "link";
navigate("register");
});
}
}
customElements.define("merchant-screen-link", MerchantScreenLink);
})();
4. OTP Ekranı
/**
* OTP doğrulama ekranı — tek dosya olarak kopyalanabilir.
* Bağımlılık: styles.css; MerchantDemo.state; merchant:navigate.
* Host üzerinde reset() çağrılabilir (shell uygulaması OTP ekranına geçerken kullanır).
*/
(function () {
"use strict";
function navigate(detail) {
document.dispatchEvent(new CustomEvent("merchant:navigate", { detail }));
}
function randomRef() {
const chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
let s = "";
for (let i = 0; i < 10; i++) s += chars[Math.floor(Math.random() * chars.length)];
return s;
}
class MerchantScreenOtp extends HTMLElement {
constructor() {
super();
this._timerId = null;
this._duration = 60;
this._remaining = 60;
this._circumference = 2 * Math.PI * 26;
}
connectedCallback() {
if (this._ready) return;
this._ready = true;
this.classList.add("screen");
this.innerHTML = `
<div class="otp-inner">
<header class="otp-header">
<button type="button" class="icon-btn otp-back" aria-label="Geri">‹</button>
<h1 class="otp-title">Kart Sahibi Doğrulama</h1>
</header>
<hr class="otp-divider" />
<p class="otp-instruction" id="otp-instruction"></p>
<p class="otp-ref" id="otp-ref"></p>
<div class="otp-cells" id="otp-cells"></div>
<div class="otp-footer">
<button type="button" class="otp-resend" id="otp-resend">SMS alamadınız mı?</button>
<div class="timer-wrap">
<svg class="timer-svg" viewBox="0 0 64 64" aria-hidden="true">
<circle class="timer-bg" cx="32" cy="32" r="26" />
<circle class="timer-progress" cx="32" cy="32" r="26"
stroke-dasharray="${this._circumference}"
stroke-dashoffset="0" />
</svg>
<span class="timer-text" id="otp-timer-text">01:00</span>
</div>
</div>
</div>
`;
const cells = this.querySelector("#otp-cells");
for (let i = 0; i < 6; i++) {
const inp = document.createElement("input");
inp.type = "text";
inp.inputMode = "numeric";
inp.maxLength = 1;
inp.className = "otp-cell";
inp.setAttribute("data-i", String(i));
inp.setAttribute("aria-label", `Kod ${i + 1}`);
cells.appendChild(inp);
}
this.querySelector(".otp-back").addEventListener("click", () => {
console.log("[otp] Geri");
this._stopTimer();
const state = window.MerchantDemo && window.MerchantDemo.state;
const back = (state && state.otpReturnScreen) || "register";
navigate({ screen: back });
});
this.querySelector("#otp-resend").addEventListener("click", async () => {
await window.MerchantDemo.state.client.otpResend();
this.reset();
});
const inputs = [...this.querySelectorAll(".otp-cell")];
const updateStyles = () => {
const vals = inputs.map((el) => el.value);
const firstEmpty = vals.findIndex((v) => !v);
inputs.forEach((el, i) => {
el.classList.toggle("otp-cell--filled", Boolean(vals[i]));
el.classList.toggle("otp-cell--active", firstEmpty !== -1 && i === firstEmpty);
});
};
const checkComplete =async () => {
const code = inputs.map((el) => el.value).join("");
if (code.length === 6) {
console.log("[otp] 6 haneli kod tamamlandı:", code);
const otpResponse = await window.MerchantDemo.state.client.otpControl(code);
if(otpResponse.data != null){
const checkWalletResponse = await window.MerchantDemo.state.client.checkWallet();
window.MerchantDemo.state.cardList = checkWalletResponse.data.cards;
const savedScreen = document.getElementById('screen-saved');
savedScreen.cards = window.MerchantDemo.state.cardList; // <-- property set
if(otpResponse.data.screen == 'WalletScreen'){
console.log("walletscreen", otpResponse.data.screen);
//this._stopTimer();
navigate({screen: "saved"});
}
else navigate({ action: "otp-complete" });
}
}
};
inputs.forEach((el, i) => {
el.addEventListener("input", (ev) => {
let v = ev.target.value.replace(/\D/g, "");
if (v.length > 1) v = v.slice(-1);
ev.target.value = v;
console.log("[otp] Hücre girişi:", { index: i, value: v, partial: inputs.map((x) => x.value).join("") });
updateStyles();
if (v && i < 5) inputs[i + 1].focus();
checkComplete();
});
el.addEventListener("keydown", (ev) => {
if (ev.key === "Backspace" && !ev.target.value && i > 0) {
inputs[i - 1].focus();
console.log("[otp] Backspace — önceki hücreye geçildi");
}
});
el.addEventListener("focus", () => {
console.log("[otp] Odak:", i);
updateStyles();
});
});
this._inputs = inputs;
this._updateStyles = updateStyles;
}
reset() {
const state = window.MerchantDemo && window.MerchantDemo.state;
const ctx = state && state.otpContext;
if (state && ctx) {
ctx.reference = randomRef();
}
const instr = this.querySelector("#otp-instruction");
const refEl = this.querySelector("#otp-ref");
if (instr && ctx) {
instr.textContent = `${ctx.phone} no'lu telefona ${ctx.bank} tarafından SMS ile gönderilen doğrulama kodunu giriniz.`;
}
if (refEl && ctx) {
refEl.textContent = `Referans No: ${ctx.reference}`;
}
this._inputs.forEach((el) => {
el.value = "";
});
this._remaining = this._duration;
this._updateTimerVisual();
this._updateStyles();
if (this._inputs[0]) this._inputs[0].focus();
this._stopTimer();
this._timerId = window.setInterval(() => {
this._remaining -= 1;
this._updateTimerVisual();
if (this._remaining <= 0) this._stopTimer();
}, 1000);
}
_stopTimer() {
if (this._timerId) {
clearInterval(this._timerId);
this._timerId = null;
}
}
_updateTimerVisual() {
const text = this.querySelector("#otp-timer-text");
const prog = this.querySelector(".timer-progress");
const m = Math.floor(Math.max(0, this._remaining) / 60);
const s = Math.max(0, this._remaining) % 60;
if (text) text.textContent = `${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`;
if (prog) {
const p = this._remaining / this._duration;
prog.style.strokeDashoffset = String(this._circumference * (1 - p));
}
}
}
customElements.define("merchant-screen-otp", MerchantScreenOtp);
})();
5. Kayıtlı Kartlar ve Kart Ekleme Ekranı
/**
* Kayıtlı kartlar ekranı — tek dosya olarak kopyalanabilir.
* Bağımlılık: styles.css; MerchantDemo.state; merchant:navigate.
*/
(() => {
"use strict";
// ------------------- Yardımcı Fonksiyonlar -------------------
const navigate = (screen) => {
document.dispatchEvent(
new CustomEvent("merchant:navigate", { detail: { screen } })
);
};
// ------------------- Bileşen Tanımı -------------------
class MerchantScreenSaved extends HTMLElement {
/* ---------- 1) observedAttributes ---------- */
static get observedAttributes() {
return ["cards"]; // JSON string olarak gönderilebilir
}
constructor() {
super();
this._ready = false; // connectedCallback içinde bir kez çalıştırılır
this._cards = window.MerchantDemo.state.cardList; // gerçek veri kaynağı
}
/* ---------- 2) Attribute değişikliği ---------- */
attributeChangedCallback(name, oldVal, newVal) {
if (name === "cards" && oldVal !== newVal) {
try {
const parsed = JSON.parse(newVal);
if (Array.isArray(parsed)) this.cards = parsed; // setter tetiklenir
} catch (e) {
console.warn("[merchant-screen-saved] cards attribute geçersiz JSON:", e);
}
}
}
/* ---------- 3) Public property (getter / setter) ---------- */
get cards() {
return this._cards;
}
set cards(value) {
if (!Array.isArray(value)) {
console.error("[merchant-screen-saved] cards must be an array");
return;
}
this._cards = value;
// Eğer bileşen zaten render edilmişse sadece kart listesini yenile
if (this._ready) this._renderCards();
}
/* ---------- 4) Bağlantı kurulduğunda bütün UI oluşturulur ---------- */
connectedCallback() {
if (this._ready) return;
this._ready = true;
this.classList.add("screen");
/* ---- Sabit UI (header, tabs, footer vb.) ---- */
this.innerHTML = `
<div class="saved-inner">
<header class="saved-header">
<button type="button" class="icon-btn saved-back" aria-label="Geri">‹</button>
<h1 class="saved-title">Kayıtlı Kartlar ile Öde</h1>
</header>
<div class="tabs" role="tablist">
<button type="button" class="tab tab--active" data-tab="list" role="tab">Kayıtlı Kartlarım</button>
<button type="button" class="tab" data-tab="new" role="tab">Yeni Kart</button>
</div>
<div class="tab-panel tab-panel--active" data-panel="list" role="tabpanel">
<!-- KART LİSTESİ buraya eklenecek -->
<div class="cards-container"></div>
</div>
<div class="tab-panel" data-panel="new" role="tabpanel">
<div class="new-card-form">
<div id = "card-field"></div>
<div id = "agreement-field"></div>
<div class="bkm-box">
<div class="bkm-head">BKM Express</div>
<div class="bkm-body">
Kartınızı BKM Express'e kaydederek ödemelerinizi hızlı ve güvenli şekilde yapabilirsiniz.
<div class="bkm-row">
<input type="checkbox" id="saved-bkm" />
<span>
<button type="button" class="link-inline" data-l="s">BKM Express Kullanıcı Sözleşmesi</button>'ni okudum ve kabul ediyorum.
Detaylı bilgi için <button type="button" class="link-inline" data-l="a">Aydınlatma Metni</button>'ni inceleyiniz.
</span>
</div>
</div>
</div>
<button type="button" class="btn-teal-full" id="saved-new-submit">Kartı Ekle</button>
</div>
</div>
</div>
<div class="saved-footer" data-footer="list">
<button type="button" class="btn-pay" id="btn-delete-saved">Kartı Sil</button>
<button type="button" class="btn-pay" id="btn-pay-saved">₺ 100,00 Öde</button>
</div>
`;
/* ---- Kısayol selectorler ---- */
this.$ = (sel) => this.querySelector(sel);
this.$$ = (sel) => this.querySelectorAll(sel);
/* ---- UI‑event handlerlar ---- */
this._bindStaticEvents();
this._renderCards(); // ilk defa kartları çiz
}
/* -----------------------------------------------------------------
5) Kart listesini sadece bu metod içinde (yeniden) render ediyoruz.
----------------------------------------------------------------- */
_renderCards() {
const container = this.$(".cards-container");
if (!container) return;
// kart HTML’i
const cardsHtml = this._cards
.map(
(c, i) => `
<div class="card-row ${i === 0 ? "card-row--selected" : ""}"
data-index="${i}" data-card-id="${c.id}" role="button" tabindex="0">
<div class="bank-logo" aria-hidden="true">
<img class="card-url" src=${c.imageUrl} />
</div>
<div class="card-info">
<div class="bank-name">${c.bankInformation.bankShortName}</div>
<div class="card-nick">${c.cardAlias}</div>
<div class="card-pan">${c.maskCardNumber}</div>
</div>
<div class="card-meta">
<div class="troy">troy</div>
<input type="radio" class="radio" name="saved-card"
${i === 0 ? "checked" : ""} data-index="${i}" data-card-id="${c.id}" aria-label="Seç" />
</div>
</div>
`
)
.join("");
container.innerHTML = cardsHtml;
// kart seçimi & radyo butonları
this._bindCardEvents();
}
/* -----------------------------------------------------------------
6) Sabit (değişmeyen) UI elementlerine event bağlama
----------------------------------------------------------------- */
_bindStaticEvents() {
// geri butonu
this.$(".saved-back").addEventListener("click", () => {
console.log("[kayıtlı kartlar] Geri");
navigate("init");
});
// sekme (list / new) ve alt‑sekme (pill) geçişi
const setTab = (name) => {
this.$$(".tab").forEach((t) =>
t.classList.toggle(
"tab--active",
t.dataset.tab === name
)
);
this.$$(".tab-panel").forEach((p) =>
p.classList.toggle(
"tab-panel--active",
p.dataset.panel === name
)
);
const footer = this.$("[data-footer]");
if (footer) footer.style.display = name === "list" ? "" : "none";
this.$$(".tab-pill").forEach((t) =>
t.classList.toggle(
"tab-pill--active",
t.dataset.subtab === name
)
);
console.log("[kayıtlı kartlar] Sekme:", name);
};
this.$$(".tab").forEach((btn) =>
btn.addEventListener("click", () => setTab(btn.dataset.tab))
);
this.$$(".tab-pill").forEach((btn) =>
btn.addEventListener("click", () => setTab(btn.dataset.subtab))
);
// yeni kart formu
this.$("#saved-bkm").addEventListener("change", (e) =>
console.log("[kayıtlı/yeni kart] BKM checkbox:", e.target.checked)
);
this.addEventListener("click", (ev) => {
const lb = ev.target.closest(".link-inline");
if (lb) console.log("[kayıtlı/yeni kart] link:", lb.dataset.l);
});
// yeni kart “Kartı Seç” butonu
this.$("#saved-new-submit").addEventListener("click", async () => {
console.log("saved-new-submit");
const resp = await window.MerchantDemo.state.client.cardInsertSecure("test kartım");
console.log(resp);
if (resp.data.screen == 'EnterOtpScreen') //state.otpReturnScreen = "saved";
navigate("otp");
});
// “Öde” butonu (listeden)
this.$("#btn-pay-saved").addEventListener("click", async () => {
const checked = this.querySelector(
'.radio[name="saved-card"]:checked'
);
const idx = checked ? Number(checked.dataset.index) : -1;
console.log("[kayıtlı kartlar] Öde — seçili kart:", idx, this._cards[idx]);
// Örnek API çağrısı – gerçek proje içinde değiştirin
const client = window.MerchantDemo?.state?.client;
if (!client) {
console.warn("[merchant-screen-saved] client objesi bulunamadı");
return;
}
try {
const d = new Date();
const transactionDate = d.toISOString()
.replace('Z', '+03:00')
.replace(/\.(\d{3})/, (_, ms) => '.' + (ms + '00').slice(0, 5));
const transactionId = crypto.randomUUID();
const orderId = crypto.randomUUID();
const resp = await client.startPayment(
this._cards[idx].cardId, new Number("50.00"), transactionDate, "949", 1, orderId, transactionId, "3D"
);
console.log(resp);
//const resp3 = await client.merchantTransactionComplete();
//console.log("merchantTransactionComplete: ", resp3);
const resp2 = await client.transactionResultControl(
resp.data.paymentToken
);
console.dir(resp2);
} catch (e) {
console.error("[merchant-screen-saved] ödeme hatası:", e);
}
});
// “Öde” butonu (listeden)
this.$("#btn-delete-saved").addEventListener("click", async () => {
const checked = this.querySelector(
'.radio[name="saved-card"]:checked'
);
const idx = checked ? Number(checked.dataset.index) : -1;
console.log("[kayıtlı kartlar] Öde — seçili kart:", idx, this._cards[idx]);
// Örnek API çağrısı – gerçek proje içinde değiştirin
const client = window.MerchantDemo?.state?.client;
if (!client) {
console.warn("[merchant-screen-saved] client objesi bulunamadı");
return;
}
try {
const resp = await client.cardDelete(
this._cards[idx].cardId
);
} catch (e) {
console.error("[merchant-screen-saved] ödeme hatası:", e);
}
});
}
/* -----------------------------------------------------------------
7) Kart satırları (list tab) için etkileşim
----------------------------------------------------------------- */
_bindCardEvents() {
const selectCard = (index) => {
this.$$(".card-row").forEach((row, i) => {
row.classList.toggle("card-row--selected", i === index);
});
this.$$(".radio[name='saved-card']").forEach((r, i) => {
r.checked = i === index;
});
console.log("[kayıtlı kartlar] Seçilen kart index:", index);
};
// satır (click / keyboard) -> radio
this.$$(".card-row").forEach((row) => {
const idx = Number(row.dataset.index);
row.addEventListener("click", () => selectCard(idx));
row.addEventListener("keydown", (ev) => {
if (ev.key === "Enter" || ev.key === " ") {
ev.preventDefault();
selectCard(idx);
}
});
});
// radio doğrudan değiştiğinde satırı da seç
this.$$(".radio[name='saved-card']").forEach((r) => {
r.addEventListener("change", (ev) => {
const idx = Number(ev.target.dataset.index);
selectCard(idx);
});
// radio tıklandığında satırın click event’i tetiklenmesin
r.addEventListener("click", (ev) => ev.stopPropagation());
});
}
}
// --------------------------------------------------------------
customElements.define("merchant-screen-saved", MerchantScreenSaved);
})();
6. Style CSS
/* merchant-web-demo — ortak + merchant-screen-* ekran stilleri */
:root {
--red-primary: #e31e24;
--red-dark: #c41e1e;
--teal: #00b5c4;
--teal-dark: #008075;
--teal-otp: #00b5c1;
--purple: #8e44ad;
--bg-page: #f5f5f5;
--border: #e0e0e0;
--text: #222;
--text-muted: #666;
--radius: 12px;
--radius-pill: 999px;
--font: system-ui, -apple-system, "Segoe UI", Roboto, Arial, sans-serif;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
body {
margin: 0;
font-family: var(--font);
color: var(--text);
background: #fff;
min-height: 100vh;
}
#app {
min-height: 100vh;
}
.screen {
display: none;
min-height: 100vh;
}
.screen--active {
display: block;
}
.screen[hidden] {
display: none !important;
}
.shell {
max-width: 420px;
margin: 0 auto;
padding: 24px 20px 32px;
}
.shell--page {
background: var(--bg-page);
min-height: 100vh;
max-width: 480px;
}
.init-title {
text-align: center;
font-size: 1.1rem;
font-weight: 700;
margin: 0 0 12px;
}
.divider {
border: none;
border-top: 1px solid var(--border);
margin: 0 0 24px;
}
.field-label {
display: block;
font-size: 0.9rem;
margin-bottom: 8px;
}
.input {
width: 100%;
padding: 14px 16px;
font-size: 1rem;
border: 1px solid var(--border);
border-radius: var(--radius);
font-family: inherit;
}
.input:focus {
outline: 2px solid var(--teal);
outline-offset: 0;
}
.input--rounded {
border-radius: 10px;
}
.route-hint {
font-size: 0.75rem;
color: var(--text-muted);
margin: 12px 0 20px;
line-height: 1.4;
}
.btn {
font-family: inherit;
font-weight: 700;
cursor: pointer;
border: none;
border-radius: 10px;
padding: 14px 20px;
font-size: 1rem;
}
.btn--primary-red {
background: var(--red-primary);
color: #fff;
}
.btn--primary-red:hover {
background: var(--red-dark);
}
.btn--block {
width: 100%;
}
/* Page header (link + payment) */
.page-header {
display: grid;
grid-template-columns: 40px 1fr 40px;
align-items: center;
padding-top: 8px;
}
.page-title {
grid-column: 1 / -1;
grid-row: 1;
text-align: center;
font-size: 1rem;
font-weight: 700;
margin: 0;
}
.icon-btn {
grid-column: 1;
grid-row: 1;
z-index: 1;
background: none;
border: none;
font-size: 1.75rem;
line-height: 1;
cursor: pointer;
color: var(--text);
padding: 4px;
}
/* ---------- merchant-screen-register ---------- */
merchant-screen-register {
display: block;
min-height: 100vh;
background: #fff;
}
merchant-screen-register .reg-inner {
max-width: 420px;
margin: 0 auto;
padding: 16px 20px 100px;
}
merchant-screen-register .reg-header {
display: grid;
grid-template-columns: 40px 1fr 40px;
align-items: center;
}
merchant-screen-register .reg-title {
grid-column: 1 / -1;
grid-row: 1;
text-align: center;
font-size: 1rem;
font-weight: 700;
margin: 0 0 12px;
padding-top: 4px;
}
merchant-screen-register .reg-back {
grid-column: 1;
grid-row: 1;
z-index: 1;
}
merchant-screen-register .input-wrap {
position: relative;
margin-bottom: 14px;
}
merchant-screen-register .input-wrap .input {
padding-right: 48px;
}
merchant-screen-register .cam-btn {
position: absolute;
right: 10px;
top: 50%;
transform: translateY(-50%);
background: none;
border: none;
cursor: pointer;
font-size: 1.25rem;
padding: 4px;
line-height: 1;
}
merchant-screen-register .bkm-box {
border: 2px solid var(--teal);
border-radius: var(--radius);
overflow: hidden;
margin: 20px 0;
}
merchant-screen-register .bkm-head {
background: var(--teal);
color: #fff;
padding: 12px 16px;
font-weight: 700;
font-style: italic;
letter-spacing: 0.02em;
}
merchant-screen-register .bkm-body {
padding: 14px 16px;
font-size: 0.85rem;
color: var(--text-muted);
line-height: 1.45;
}
merchant-screen-register .bkm-row {
display: flex;
gap: 10px;
align-items: flex-start;
margin-top: 12px;
}
merchant-screen-register .bkm-row input[type="checkbox"] {
width: 20px;
height: 20px;
accent-color: var(--teal);
margin-top: 2px;
flex-shrink: 0;
}
merchant-screen-register .link {
color: var(--teal);
text-decoration: underline;
cursor: pointer;
background: none;
border: none;
font: inherit;
padding: 0;
}
merchant-screen-register .reg-footer {
position: fixed;
bottom: 0;
left: 0;
right: 0;
padding: 16px 20px 24px;
background: linear-gradient(transparent, #fff 30%);
max-width: 420px;
margin: 0 auto;
}
merchant-screen-register .btn-teal {
width: 100%;
background: var(--teal);
color: #fff;
border-radius: var(--radius);
padding: 16px;
font-weight: 700;
border: none;
cursor: pointer;
font-size: 1rem;
}
merchant-screen-register .btn-teal:hover {
filter: brightness(0.95);
}
/* ---------- merchant-screen-link ---------- */
merchant-screen-link {
display: block;
padding: 8px 0 24px;
}
merchant-screen-link .link-card {
background: #fff;
border-radius: 16px;
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.08);
overflow: hidden;
}
merchant-screen-link .link-card-head {
display: flex;
align-items: center;
justify-content: space-between;
background: var(--teal-dark);
color: #fff;
padding: 16px 18px;
}
merchant-screen-link .bkm-brand {
font-weight: 800;
font-size: 1.25rem;
}
merchant-screen-link .bkm-brand small {
display: block;
font-weight: 400;
font-style: italic;
font-size: 0.85rem;
opacity: 0.95;
}
merchant-screen-link .btn-link-account {
background: #fff;
color: var(--teal-dark);
border: none;
font-weight: 700;
padding: 10px 14px;
border-radius: 10px;
cursor: pointer;
font-size: 0.8rem;
white-space: nowrap;
}
merchant-screen-link .link-card-body {
padding: 20px 18px 24px;
}
merchant-screen-link .link-card-body h2 {
font-size: 1rem;
margin: 0 0 10px;
}
merchant-screen-link .link-card-body p {
margin: 0 0 18px;
font-size: 0.88rem;
color: var(--text-muted);
line-height: 1.5;
}
merchant-screen-link .or {
text-align: center;
color: #aaa;
font-size: 0.9rem;
margin: 20px 0 14px;
}
merchant-screen-link .btn-outline-purple {
width: 100%;
background: transparent;
border: 2px solid var(--purple);
color: var(--purple);
font-weight: 700;
padding: 14px;
border-radius: var(--radius);
cursor: pointer;
font-size: 0.95rem;
}
/* ---------- merchant-screen-saved ---------- */
merchant-screen-saved {
display: block;
min-height: 100vh;
background: #fff;
}
merchant-screen-saved .saved-inner {
max-width: 420px;
margin: 0 auto;
padding: 12px 16px 100px;
}
merchant-screen-saved .saved-header {
display: grid;
grid-template-columns: 40px 1fr;
align-items: center;
margin-bottom: 8px;
}
merchant-screen-saved .saved-title {
text-align: center;
font-size: 1rem;
font-weight: 700;
margin: 0;
grid-column: 1 / -1;
grid-row: 1;
}
merchant-screen-saved .saved-back {
grid-column: 1;
grid-row: 1;
z-index: 1;
}
merchant-screen-saved .tabs {
display: flex;
border-bottom: 2px solid var(--border);
margin-bottom: 16px;
}
merchant-screen-saved .tab {
flex: 1;
padding: 12px 8px;
text-align: center;
background: none;
border: none;
font: inherit;
font-weight: 600;
color: var(--text-muted);
cursor: pointer;
border-bottom: 3px solid transparent;
margin-bottom: -2px;
}
merchant-screen-saved .tab--active {
color: var(--red-primary);
border-bottom-color: var(--red-primary);
}
merchant-screen-saved .tab-panel {
display: none;
}
merchant-screen-saved .tab-panel--active {
display: block;
}
/* Modal-style tabs (yeni kart teal) — alt görünüm */
merchant-screen-saved .tabs-pill {
display: flex;
background: #f0f0f0;
border-radius: var(--radius-pill);
padding: 4px;
margin-bottom: 18px;
}
merchant-screen-saved .tab-pill {
flex: 1;
border: none;
background: transparent;
padding: 10px 12px;
border-radius: var(--radius-pill);
font: inherit;
font-weight: 600;
cursor: pointer;
color: var(--text);
}
merchant-screen-saved .tab-pill--active {
background: var(--teal);
color: #fff;
}
merchant-screen-saved .card-row {
display: flex;
align-items: center;
gap: 12px;
padding: 14px;
border: 2px solid var(--border);
border-radius: var(--radius);
margin-bottom: 10px;
cursor: pointer;
}
merchant-screen-saved .card-row--selected {
border-color: var(--red-primary);
}
merchant-screen-saved .bank-logo {
width: 40px;
height: 40px;
border-radius: 8px;
background: linear-gradient(135deg, #2d8f4e, #c9a227);
flex-shrink: 0;
}
merchant-screen-saved .card-url {
height: 40px;
object-fit: cover ;
}
merchant-screen-saved .card-info {
flex: 1;
min-width: 0;
margin-left: 30px;
}
merchant-screen-saved .bank-name {
font-weight: 700;
font-size: 0.95rem;
}
merchant-screen-saved .card-nick {
font-size: 0.8rem;
color: var(--text-muted);
}
merchant-screen-saved .card-pan {
font-size: 0.8rem;
color: #888;
font-family: ui-monospace, monospace;
margin-top: 4px;
}
merchant-screen-saved .card-meta {
text-align: right;
flex-shrink: 0;
}
merchant-screen-saved .troy {
color: #1a56c4;
font-size: 0.75rem;
font-weight: 600;
text-transform: lowercase;
}
merchant-screen-saved .radio {
width: 22px;
height: 22px;
accent-color: var(--red-primary);
margin-top: 8px;
}
merchant-screen-saved .info-banner {
display: flex;
gap: 12px;
align-items: flex-start;
background: #f5f5f5;
border-radius: var(--radius);
padding: 14px;
margin: 16px 0;
font-size: 0.8rem;
color: var(--text-muted);
line-height: 1.4;
}
merchant-screen-saved .info-icon {
width: 28px;
height: 28px;
border-radius: 50%;
background: #555;
color: #fff;
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: 0.85rem;
flex-shrink: 0;
}
merchant-screen-saved .saved-footer {
position: fixed;
bottom: 0;
left: 0;
right: 0;
padding: 16px 20px 24px;
background: #fff;
box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.06);
max-width: 420px;
margin: 0 auto;
}
merchant-screen-saved .btn-pay {
width: 100%;
background: var(--red-primary);
color: #fff;
border: none;
border-radius: var(--radius);
padding: 16px;
margin: 10px;
font-weight: 700;
font-size: 1rem;
cursor: pointer;
}
/* Yeni kart formu (saved içinde) */
merchant-screen-saved .new-card-form .input-wrap {
position: relative;
margin-bottom: 12px;
}
merchant-screen-saved .new-card-form .input {
width: 100%;
padding: 14px 44px 14px 14px;
border: 1px solid var(--border);
border-radius: var(--radius);
font-size: 1rem;
}
merchant-screen-saved .new-card-form .input:focus {
border-color: var(--teal);
outline: none;
}
merchant-screen-saved .cam-btn {
position: absolute;
right: 10px;
top: 50%;
transform: translateY(-50%);
background: none;
border: none;
cursor: pointer;
font-size: 1.2rem;
}
merchant-screen-saved .bkm-box {
border: 2px solid var(--teal);
border-radius: var(--radius);
overflow: hidden;
margin: 16px 0;
}
merchant-screen-saved .bkm-head {
background: var(--teal);
color: #fff;
padding: 12px 16px;
font-weight: 700;
font-style: italic;
}
merchant-screen-saved .bkm-body {
padding: 12px 14px;
font-size: 0.82rem;
color: var(--text-muted);
}
merchant-screen-saved .bkm-row {
display: flex;
gap: 10px;
margin-top: 10px;
align-items: flex-start;
}
merchant-screen-saved .bkm-row input {
accent-color: var(--teal);
width: 20px;
height: 20px;
margin-top: 2px;
}
merchant-screen-saved .link-inline {
background: none;
border: none;
color: var(--teal);
text-decoration: underline;
cursor: pointer;
font: inherit;
padding: 0;
}
merchant-screen-saved .btn-teal-full {
width: 100%;
margin-top: 8px;
background: var(--teal);
color: #fff;
border: none;
border-radius: var(--radius-pill);
padding: 16px;
font-weight: 700;
cursor: pointer;
font-size: 1rem;
}
/* ---------- merchant-screen-otp ---------- */
merchant-screen-otp {
display: block;
min-height: 100vh;
background: #fff;
}
merchant-screen-otp .otp-inner {
max-width: 420px;
margin: 0 auto;
padding: 16px 20px 32px;
}
merchant-screen-otp .otp-header {
display: grid;
grid-template-columns: 40px 1fr;
align-items: center;
}
merchant-screen-otp .otp-title {
grid-column: 1 / -1;
grid-row: 1;
text-align: center;
font-size: 1rem;
font-weight: 700;
margin: 0 0 8px;
}
merchant-screen-otp .otp-back {
grid-column: 1;
grid-row: 1;
z-index: 1;
}
merchant-screen-otp .otp-divider {
border: none;
border-top: 1px solid var(--border);
margin: 0 0 20px;
}
merchant-screen-otp .otp-instruction {
font-size: 0.9rem;
color: var(--text-muted);
line-height: 1.5;
margin: 0 0 8px;
}
merchant-screen-otp .otp-ref {
font-weight: 700;
margin: 0 0 24px;
font-size: 0.95rem;
}
merchant-screen-otp .otp-cells {
display: flex;
gap: 8px;
justify-content: space-between;
margin-bottom: 32px;
}
merchant-screen-otp .otp-cell {
width: 100%;
max-width: 52px;
aspect-ratio: 1;
text-align: center;
font-size: 1.25rem;
font-weight: 700;
border: 2px solid var(--border);
border-radius: 10px;
font-family: inherit;
padding: 0;
}
merchant-screen-otp .otp-cell:focus {
border-color: var(--teal-otp);
outline: none;
}
merchant-screen-otp .otp-cell--filled {
background: var(--teal-otp);
border-color: var(--teal-otp);
color: #fff;
}
merchant-screen-otp .otp-cell--active:not(.otp-cell--filled) {
border-color: var(--teal-otp);
}
merchant-screen-otp .otp-footer {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
}
merchant-screen-otp .otp-resend {
background: none;
border: none;
color: var(--text-muted);
font: inherit;
font-size: 0.88rem;
cursor: pointer;
text-align: left;
padding: 0;
}
merchant-screen-otp .timer-wrap {
position: relative;
width: 64px;
height: 64px;
flex-shrink: 0;
}
merchant-screen-otp .timer-svg {
transform: rotate(-90deg);
width: 64px;
height: 64px;
}
merchant-screen-otp .timer-bg {
fill: none;
stroke: #eee;
stroke-width: 4;
}
merchant-screen-otp .timer-progress {
fill: none;
stroke: var(--teal-otp);
stroke-width: 4;
stroke-linecap: round;
transition: stroke-dashoffset 0.35s linear;
}
merchant-screen-otp .timer-text {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.75rem;
font-weight: 700;
font-variant-numeric: tabular-nums;
}
/* ---------- merchant-screen-payment ---------- */
merchant-screen-payment {
display: block;
min-height: 100vh;
background: var(--bg-page);
}
merchant-screen-payment .pay-inner {
max-width: 420px;
margin: 0 auto;
padding: 16px 16px 32px;
}
merchant-screen-payment .pay-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 20px;
}
merchant-screen-payment .pay-title {
font-size: 1.05rem;
font-weight: 700;
margin: 0;
}
merchant-screen-payment .pay-card {
background: #fff;
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 20px;
margin-bottom: 14px;
}
merchant-screen-payment .btn-add-method {
display: inline-flex;
align-items: center;
gap: 8px;
margin: 0 auto;
padding: 10px 20px;
border: 1px solid var(--border);
border-radius: var(--radius-pill);
background: #fff;
font: inherit;
font-weight: 600;
cursor: pointer;
}
merchant-screen-payment .pay-card--center {
display: flex;
justify-content: center;
}
merchant-screen-payment .addr-row {
display: flex;
align-items: center;
gap: 14px;
cursor: pointer;
width: 100%;
text-align: left;
background: none;
border: none;
font: inherit;
padding: 0;
}
merchant-screen-payment .addr-pin {
font-size: 1.5rem;
}
merchant-screen-payment .addr-text {
flex: 1;
}
merchant-screen-payment .addr-line1 {
font-weight: 700;
margin-bottom: 4px;
}
merchant-screen-payment .addr-line2 {
font-size: 0.88rem;
color: var(--text-muted);
}
merchant-screen-payment .addr-arrow {
color: #999;
font-size: 1.25rem;
}
merchant-screen-payment .summary-row {
display: flex;
justify-content: space-between;
padding: 8px 0;
font-size: 0.92rem;
}
merchant-screen-payment .summary-row--total {
border-top: 1px solid var(--border);
margin-top: 8px;
padding-top: 14px;
font-weight: 700;
font-size: 1.05rem;
}
.card-form {
background: #fff;
padding: 2rem 1.5rem;
border-radius: 12px;
box-shadow: 0 8px 20px rgba(0, 0, 0, .08);
width: 100%;
max-width: 420px;
animation: slideUp .4s ease-out;
display: flex;
flex-direction: column;
}
#card-field {
height: 300px;
}
#card-field-register {
height: 300px;
}