Compare commits

...

2 Commits

Author SHA1 Message Date
CIFO Dev 0201274182 payments: konsistenkan varian tombol dan penataan VA
- Ubah semua Button varian 'ghost' menjadi 'outline' di CardPanel, GoPayPanel, CStorePanel, InlinePaymentStatus untuk konsistensi tipe dan styling\n- Hilangkan logo per-bank dari BankTransferPanel; hanya tampilkan nama bank\n- Perbaiki tampilan Nomor VA: pindah ke baris baru, wrap (reak-all), ukuran font responsif, kurangi tracking\n- Tampilkan gambar 'logo semua bank' di header metode Transfer bank (kanan) dan panel pilihan di Step 2\n- Perbesar ikon logo agar jelas dan seragam di PaymentMethodList\n- Penyesuaian ringan di PaymentSheet untuk CTA status
2025-11-10 14:50:55 +07:00
CIFO Dev b07c267704 chore(assets): point bank logos to files in public/logos
PaymentLogos: update BANK_LOGOS src to match current asset filenames (BCA, BNI, BRI, CIMB, Mandiri, Permata). Ensures PaymentMethodList and panels render new logos.
2025-11-10 13:28:58 +07:00
22 changed files with 566 additions and 102 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

BIN
public/logos/BRI_2025.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

View File

@ -0,0 +1,142 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="59.999561mm"
height="17.510006mm"
viewBox="0 0 59.999561 17.510006"
version="1.1"
id="svg945"
inkscape:version="0.92.2 (5c3e80d, 2017-08-06)"
sodipodi:docname="Bank Mandiri logo 2016.svg">
<defs
id="defs939">
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath20">
<path
d="M 382.677,748.082 H 552.755 V 799.37 H 382.677 Z"
id="path18"
inkscape:connector-curvature="0" />
</clipPath>
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.35"
inkscape:cx="140.52785"
inkscape:cy="-472.62453"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
inkscape:window-width="1600"
inkscape:window-height="837"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1" />
<metadata
id="metadata942">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(60.237875,-6.2747549)">
<g
transform="matrix(0.35277777,0,0,-0.35277777,-195.23799,287.69146)"
id="g14">
<g
id="g16"
clip-path="url(#clipPath20)">
<g
id="g22"
transform="translate(382.8575,763.6027)">
<path
d="M 0,0 C 0,2.465 -0.047,4.533 -0.18,6.378 H 4.484 L 4.703,3.211 h 0.13 c 1.055,1.674 2.992,3.654 6.598,3.654 2.815,0 5.013,-1.59 5.936,-3.962 h 0.09 c 0.751,1.189 1.629,2.065 2.638,2.683 1.187,0.835 2.552,1.279 4.314,1.279 3.558,0 7.161,-2.42 7.161,-9.285 v -12.619 h -5.276 v 11.831 c 0,3.561 -1.23,5.673 -3.821,5.673 -1.851,0 -3.217,-1.321 -3.785,-2.861 -0.132,-0.527 -0.265,-1.186 -0.265,-1.799 v -12.844 h -5.279 v 12.402 c 0,2.99 -1.186,5.102 -3.692,5.102 C 7.428,2.465 6.067,0.882 5.583,-0.611 5.364,-1.143 5.271,-1.757 5.271,-2.373 V -15.039 H 0 Z"
style="fill:#003a70;fill-opacity:1;fill-rule:nonzero;stroke:none"
id="path24"
inkscape:connector-curvature="0" />
</g>
<g
id="g26"
transform="translate(429.7098,759.028)">
<path
d="m 0,0 c -3.827,0.09 -7.475,-0.743 -7.475,-4 0,-2.108 1.364,-3.079 3.079,-3.079 2.151,0 3.738,1.407 4.217,2.946 C -0.046,-3.738 0,-3.296 0,-2.946 Z m 5.276,-5.32 c 0,-1.934 0.089,-3.818 0.311,-5.144 H 0.704 L 0.353,-8.088 H 0.216 c -1.317,-1.672 -3.559,-2.858 -6.327,-2.858 -4.312,0 -6.729,3.116 -6.729,6.377 0,5.409 4.79,8.133 12.707,8.09 v 0.352 c 0,1.408 -0.573,3.739 -4.351,3.739 -2.113,0 -4.315,-0.663 -5.763,-1.586 l -1.057,3.517 c 1.583,0.971 4.356,1.897 7.742,1.897 6.861,0 8.838,-4.358 8.838,-9.018 z"
style="fill:#003a70;fill-opacity:1;fill-rule:nonzero;stroke:none"
id="path28"
inkscape:connector-curvature="0" />
</g>
<g
id="g30"
transform="translate(438.7279,763.6027)">
<path
d="m 0,0 c 0,2.465 -0.049,4.533 -0.177,6.378 h 4.745 l 0.265,-3.21 h 0.13 c 0.923,1.668 3.257,3.697 6.816,3.697 3.742,0 7.611,-2.42 7.611,-9.193 v -12.711 h -5.411 v 12.093 c 0,3.079 -1.144,5.411 -4.089,5.411 C 7.735,2.465 6.243,0.928 5.668,-0.701 5.496,-1.187 5.449,-1.848 5.449,-2.457 V -15.039 H 0 Z"
style="fill:#003a70;fill-opacity:1;fill-rule:nonzero;stroke:none"
id="path32"
inkscape:connector-curvature="0" />
</g>
<g
id="g34"
transform="translate(476.1589,761.0557)">
<path
d="m 0,0 c 0,0.437 -0.045,0.965 -0.131,1.405 -0.485,2.111 -2.198,3.825 -4.662,3.825 -3.474,0 -5.408,-3.08 -5.408,-7.081 0,-3.914 1.934,-6.776 5.366,-6.776 2.196,0 4.133,1.497 4.659,3.828 C -0.045,-4.314 0,-3.782 0,-3.211 Z M 5.408,17.53 V -6.466 c 0,-2.198 0.09,-4.575 0.176,-6.024 H 0.747 l -0.216,3.383 h -0.09 c -1.275,-2.371 -3.876,-3.867 -6.994,-3.867 -5.1,0 -9.147,4.347 -9.147,10.946 -0.043,7.171 4.444,11.44 9.589,11.44 2.944,0 5.057,-1.233 6.023,-2.822 H 0 v 10.94 z"
style="fill:#003a70;fill-opacity:1;fill-rule:nonzero;stroke:none"
id="path36"
inkscape:connector-curvature="0" />
</g>
<path
d="m 485.391,769.981 h 5.461 v -21.415 h -5.461 z"
style="fill:#003a70;fill-opacity:1;fill-rule:nonzero;stroke:none"
id="path38"
inkscape:connector-curvature="0" />
<g
id="g40"
transform="translate(494.6713,763.0753)">
<path
d="M 0,0 C 0,2.902 -0.044,4.972 -0.177,6.904 H 4.53 L 4.704,2.818 h 0.179 c 1.054,3.034 3.562,4.086 5.849,4.086 0.527,0 0.835,0.091 1.275,0 V 2.153 C 11.567,2.243 11.084,2.331 10.421,2.331 7.829,2.331 6.07,0.661 5.588,-1.757 5.5,-2.24 5.413,-2.816 5.413,-3.434 V -14.51 H 0 Z"
style="fill:#003a70;fill-opacity:1;fill-rule:nonzero;stroke:none"
id="path42"
inkscape:connector-curvature="0" />
</g>
<path
d="m 509.185,769.981 h 5.45 v -21.415 h -5.45 z"
style="fill:#003a70;fill-opacity:1;fill-rule:nonzero;stroke:none"
id="path44"
inkscape:connector-curvature="0" />
<g
id="g46"
transform="translate(548.2855,792.6043)">
<path
d="m 0,0 c -2.691,3.065 -5.547,1.681 -7.827,0.551 -0.955,-0.472 -4.422,-2.78 -7.858,-4.389 -2.448,-1.147 -5.962,-0.8 -7.889,1.58 -0.117,0.144 -3.232,3.918 -3.562,4.383 -2.023,2.537 -7.14,4.641 -13.254,1.142 -3.276,-1.896 -10.996,-6.326 -13.882,-7.974 -1.754,-0.895 -5.826,-1.286 -8.126,1.821 -0.038,0.051 -3.06,3.822 -3.181,3.967 -0.089,0.102 -2.041,2.997 -6.392,3.086 -0.64,0.016 -3.836,0.035 -6.959,-1.738 -4.141,-2.369 -13.779,-7.879 -13.779,-7.879 -0.007,0 -0.007,-0.005 -0.007,-0.005 -3.962,-2.268 -7.052,-4.029 -7.052,-4.029 l 3.648,-4.484 c 1.708,-2.117 5.554,-3.758 8.892,-1.842 0,0 12.33,7.14 12.373,7.16 5.334,2.927 9.45,2.927 12.177,1.837 2.453,-1.033 4.585,-3.613 4.585,-3.613 0,0 2.789,-3.456 3.279,-4.06 1.586,-1.952 4.209,-1.186 4.209,-1.186 0,0 0.969,0.112 2.441,0.982 0,0 11.94,6.923 11.947,6.925 3.794,2.225 7.274,2.64 9.049,2.478 5.567,-0.507 7.298,-4.396 9.714,-7.108 1.421,-1.599 2.702,-2.503 4.663,-2.457 1.291,0.029 2.746,0.808 2.961,0.948 l 14.3,8.255 c 0,0 -1.465,2.208 -4.47,5.649"
style="fill:#ffb700;fill-opacity:1;fill-rule:evenodd;stroke:none"
id="path48"
inkscape:connector-curvature="0" />
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 7.3 KiB

View File

@ -0,0 +1,151 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="200"
height="57.930748"
viewBox="0 0 52.916666 15.327511"
version="1.1"
id="svg5028"
sodipodi:docname="Bank_Negara_Indonesia_2004.svg"
inkscape:version="1.3.2 (091e20e, 2023-11-25, custom)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<defs
id="defs5022">
<clipPath
id="clipPath112"
clipPathUnits="userSpaceOnUse">
<path
inkscape:connector-curvature="0"
id="path114"
d="M 0,0 H 595.276 V 841.89 H 0 Z" />
</clipPath>
<clipPath
id="clipPath78"
clipPathUnits="userSpaceOnUse">
<path
inkscape:connector-curvature="0"
id="path80"
d="M 0,0 H 595.276 V 841.89 H 0 Z" />
</clipPath>
<clipPath
id="clipPath56"
clipPathUnits="userSpaceOnUse">
<path
inkscape:connector-curvature="0"
id="path58"
d="M 0,0 H 595.276 V 841.89 H 0 Z" />
</clipPath>
<clipPath
id="clipPath40"
clipPathUnits="userSpaceOnUse">
<path
inkscape:connector-curvature="0"
id="path42"
d="M 0,0 H 595.276 V 841.89 H 0 Z" />
</clipPath>
<clipPath
id="clipPath26"
clipPathUnits="userSpaceOnUse">
<path
inkscape:connector-curvature="0"
id="path28"
d="M 0,0 H 595.276 V 841.89 H 0 Z" />
</clipPath>
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.7"
inkscape:cx="674.28571"
inkscape:cy="155"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
units="px"
inkscape:window-width="1920"
inkscape:window-height="1094"
inkscape:window-x="-11"
inkscape:window-y="-11"
inkscape:window-maximized="1"
inkscape:showpageshadow="2"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1" />
<metadata
id="metadata5025">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-7.903e-7,-220.36267)">
<path
d="M 14.507339,234.87054 H 1.779604e-4 V 220.36267 H 14.507339 Z"
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0530009"
id="path176"
inkscape:connector-curvature="0" />
<path
inkscape:connector-curvature="0"
id="path188"
style="fill:#f15a22;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.355742"
d="m 5.8338147,230.15493 1.5464108,-1.2483 c -0.259336,-0.3255 -0.5055096,-0.65385 -0.7374534,-0.99287 -1.788671,-2.58803 -2.8484265,-5.22087 -2.0832253,-7.55098 H 7.903e-7 v 2.51474 z" />
<path
inkscape:connector-curvature="0"
id="path192"
style="fill:#f15a22;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.355742"
d="m 8.2722841,226.39438 c 0.096762,-0.82141 0.3457812,-2.37066 1.8523489,-3.52149 1.304506,-0.99323 2.937006,-1.18426 4.382742,-0.57025 v -1.93986 H 6.4583553 c -0.5311228,2.36034 0.8640974,4.95406 1.8139288,6.0316" />
<path
inkscape:connector-curvature="0"
id="path196"
style="fill:#f15a22;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.355742"
d="m 10.22481,224.02724 -0.0057,0.004 c -1.5147497,1.23087 -1.1422877,3.16255 0.267874,4.89466 1.08608,1.33687 2.539287,2.27532 4.020241,1.56384 v -5.70326 c -1.330476,-1.36285 -2.927402,-1.86195 -4.282423,-0.75951" />
<path
inkscape:connector-curvature="0"
id="path200"
style="fill:#f15a22;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.355742"
d="m 6.9788481e-5,225.8523 v 9.01664 H 0.00220424 l 4.38914556,-3.5503 z" />
<path
inkscape:connector-curvature="0"
id="path212"
style="fill:#f15a22;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.355742"
d="m 7.9827457,229.62178 c -0.4311594,0.35432 -0.9637052,0.78797 -1.5545928,1.27071 l 0.5702546,0.71006 c 0.8242542,1.03094 2.0305756,2.81713 3.5243365,2.33367 l 0.0555,0.0679 -1.0615345,0.86481 h 4.9907055 v -3.43007 c -2.567746,1.39522 -4.7754815,0.12522 -6.5246653,-1.81713" />
<path
inkscape:connector-curvature="0"
id="path216"
style="fill:#f15a22;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.355742"
d="m 5.5741585,232.79262 -0.5908876,-0.73497 c -1.2113015,0.97225 -2.4827237,1.99145 -3.5147314,2.81143 h 5.584083 c -0.3625012,-0.76947 -1.0213354,-1.50657 -1.478464,-2.07646" />
<path
inkscape:connector-curvature="0"
id="path220"
style="fill:#006885;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.355742"
d="m 26.590332,226.42266 c 1.336878,-0.23372 2.207024,-1.3881 2.207024,-2.86123 0,-1.96298 -1.699024,-3.19848 -4.60686,-3.19848 h -5.754839 v 0.0669 c 1.175727,0.64461 1.148691,1.68515 1.148691,2.6791 v 8.99316 c 0,1.01101 0.02704,2.06223 -1.148691,2.69652 v 0.0655 h 2.771231 c 2.707196,0 4.783662,0.0462 6.333276,-0.68232 1.646374,-0.77373 2.643874,-2.1622 2.643874,-3.85517 0,-2.16078 -1.607598,-3.48627 -3.593706,-3.90392 m -4.749156,-4.72212 1.821398,0.0299 c 1.350754,0 2.731744,0.66132 2.731744,2.18247 0,1.80753 -1.22233,2.29063 -2.963686,2.29063 l -1.589456,0.002 v -0.002 z m 1.755586,11.67332 -1.755586,0.002 v -5.71748 -0.0811 l 1.887566,-0.009 c 2.196708,0 4.009926,0.81572 4.009926,2.95444 0,2.09425 -1.903576,2.8502 -4.141906,2.8502" />
<path
inkscape:connector-curvature="0"
id="path224"
style="fill:#006885;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.355742"
d="m 34.596058,232.10283 c 0,1.23727 -0.02241,2.1156 1.1462,2.80254 v 0.0623 h -3.901066 v -0.0623 c 1.16719,-0.68694 1.146912,-1.56527 1.146912,-2.80254 v -8.81208 c 0,-1.22803 0.02028,-2.20169 -1.14549,-2.85981 v -0.0679 h 3.23832 v 0.0217 c 0.08609,0.26289 0.229454,0.41052 0.329418,0.55282 0.103876,0.13732 0.308072,0.43863 0.308072,0.43863 l 7.428606,10.01201 v -8.18314 c 0,-1.23371 0.0192,-2.11595 -1.14549,-2.77407 v -0.0679 h 3.877588 v 0.0679 c -1.145132,0.65812 -1.145132,1.54036 -1.145132,2.77407 v 12.48512 c -0.708284,-0.28103 -2.683008,-1.92527 -4.089256,-3.81711 -2.524346,-3.39022 -6.048682,-8.10522 -6.048682,-8.10522 z" />
<path
inkscape:connector-curvature="0"
id="path228"
style="fill:#006885;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.355742"
d="m 49.496208,222.99167 c 0,-1.01351 -0.01744,-1.91709 -1.14371,-2.56063 v -0.0683 h 4.56417 v 0.0683 c -1.130904,0.62397 -1.140864,1.56705 -1.140864,2.56063 v 9.26886 c 0,0.99039 -0.02633,1.99429 1.140864,2.63997 v 0.0654 h -4.56417 v -0.0654 c 1.173948,-0.65493 1.14371,-1.64104 1.14371,-2.63392 z" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 7.3 KiB

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="1084pt" height="166pt" viewBox="0 0 1084 166" version="1.1" xmlns="http://www.w3.org/2000/svg">
<g id="#790008ff">
<path fill="#790008" opacity="1.00" d=" M 0.00 0.00 L 165.25 0.00 C 165.25 55.08 165.25 110.16 165.24 165.24 C 110.16 165.25 55.08 165.24 0.00 165.25 L 0.00 0.00 M 22.01 31.02 C 34.87 48.24 47.92 65.31 60.84 82.48 C 48.03 99.72 34.90 116.72 22.07 133.95 C 51.40 134.07 80.72 133.95 110.05 134.01 C 123.25 116.93 135.97 99.48 149.26 82.46 L 149.01 82.47 C 136.06 65.30 123.05 48.17 110.12 31.00 C 80.75 31.02 51.38 30.97 22.01 31.02 Z" />
<path fill="#790008" opacity="1.00" d=" M 221.25 48.29 C 231.09 36.02 246.22 28.14 261.94 27.25 C 276.83 26.10 291.98 31.33 303.17 41.18 C 298.12 46.27 293.06 51.33 287.97 56.38 C 279.36 49.40 267.40 46.65 256.68 49.71 C 244.72 52.85 235.12 63.07 232.45 75.11 C 229.19 88.18 233.90 103.03 244.65 111.31 C 256.93 121.36 275.98 120.85 288.05 110.70 C 293.11 115.77 298.28 120.75 303.21 125.96 C 293.22 134.64 280.23 139.86 266.97 140.02 C 250.26 140.48 233.52 133.13 222.61 120.46 C 214.98 111.77 210.12 100.67 209.03 89.15 C 207.50 74.63 211.97 59.58 221.25 48.29 Z" />
<path fill="#790008" opacity="1.00" d=" M 323.25 30.25 C 330.50 30.25 337.74 30.26 344.99 30.24 C 345.01 65.83 344.99 101.41 345.00 137.00 C 337.75 137.00 330.50 137.01 323.25 136.99 C 323.26 101.41 323.25 65.83 323.25 30.25 Z" />
<path fill="#790008" opacity="1.00" d=" M 371.00 30.00 C 378.05 29.98 385.10 30.04 392.16 29.97 C 404.73 52.58 416.98 75.39 429.71 97.92 C 442.05 75.24 454.57 52.67 466.91 29.99 C 474.02 30.01 481.13 29.99 488.25 30.00 C 488.21 65.60 488.32 101.19 488.19 136.79 C 480.95 136.68 473.72 136.81 466.48 136.72 C 466.53 115.65 466.48 94.59 466.51 73.53 C 454.04 94.82 442.16 116.47 429.47 137.62 C 417.35 116.19 404.90 94.95 392.73 73.55 C 392.77 94.62 392.74 115.68 392.75 136.75 C 385.50 136.75 378.26 136.74 371.01 136.76 C 370.98 101.17 371.01 65.59 371.00 30.00 Z" />
<path fill="#790008" opacity="1.00" d=" M 513.01 30.24 C 527.69 30.26 542.36 30.24 557.04 30.25 C 563.83 30.38 570.80 31.81 576.60 35.50 C 590.61 44.06 593.83 66.29 582.13 78.07 C 580.63 79.67 578.85 80.99 577.08 82.27 C 583.75 85.78 588.87 92.29 590.11 99.79 C 591.92 109.91 589.61 121.53 581.76 128.67 C 575.50 134.66 566.60 136.82 558.16 136.93 C 543.11 137.09 528.05 136.96 513.00 137.00 C 513.01 101.41 512.98 65.83 513.01 30.24 M 534.75 49.75 C 534.74 57.50 534.75 65.25 534.75 73.00 C 541.15 72.98 547.56 73.03 553.97 72.99 C 557.70 72.92 561.73 72.14 564.46 69.39 C 568.34 65.27 568.44 58.32 564.99 53.90 C 562.34 50.71 557.96 49.72 553.99 49.74 C 547.57 49.75 541.16 49.75 534.75 49.75 M 534.75 92.51 C 534.75 100.75 534.74 109.00 534.75 117.25 C 541.82 117.26 548.89 117.24 555.96 117.27 C 559.27 117.37 562.75 116.44 565.23 114.15 C 570.17 109.39 570.26 100.52 565.22 95.80 C 562.50 93.22 558.60 92.55 554.99 92.50 C 548.24 92.49 541.49 92.50 534.75 92.51 Z" />
<path fill="#790008" opacity="1.00" d=" M 635.24 30.01 C 638.87 30.05 642.50 29.88 646.13 30.06 C 647.79 31.35 648.72 33.37 650.05 34.99 C 669.97 62.20 689.73 89.53 709.76 116.66 C 709.74 87.77 709.76 58.89 709.75 30.00 C 713.50 30.00 717.25 30.00 721.00 30.00 C 721.00 65.50 721.00 101.00 721.00 136.50 C 717.21 136.50 713.42 136.49 709.64 136.52 C 688.60 107.58 667.57 78.63 646.50 49.71 C 646.50 78.64 646.50 107.57 646.50 136.49 C 642.72 136.48 638.94 136.57 635.18 136.41 C 635.35 100.95 635.21 65.48 635.24 30.01 Z" />
<path fill="#790008" opacity="1.00" d=" M 749.00 30.00 C 752.75 30.00 756.49 30.00 760.25 30.00 C 760.25 65.50 760.24 100.99 760.25 136.48 C 756.50 136.51 752.75 136.49 749.01 136.50 C 748.98 101.00 749.01 65.50 749.00 30.00 Z" />
<path fill="#790008" opacity="1.00" d=" M 777.57 137.22 C 790.54 101.71 803.52 66.20 816.58 30.73 C 821.78 30.78 826.97 30.74 832.17 30.75 C 844.54 66.00 856.89 101.27 869.30 136.50 C 864.88 136.49 860.46 136.51 856.05 136.49 C 852.76 127.06 849.42 117.64 846.03 108.24 C 831.34 108.25 816.66 108.25 801.98 108.25 C 798.70 117.90 795.55 127.61 792.26 137.26 C 787.36 137.22 782.46 137.28 777.57 137.22 M 806.48 95.48 C 818.17 95.53 829.87 95.49 841.56 95.50 C 835.70 78.42 829.92 61.31 824.16 44.20 C 818.29 61.30 812.30 78.36 806.48 95.48 Z" />
<path fill="#790008" opacity="1.00" d=" M 894.27 48.18 C 903.15 39.04 915.03 32.47 927.84 31.29 C 943.63 29.70 959.35 35.56 972.12 44.55 C 969.63 47.78 967.01 50.90 964.54 54.14 C 960.29 48.09 952.76 45.83 945.97 44.00 C 934.16 40.75 920.76 42.51 910.79 49.84 C 901.78 56.64 895.76 66.97 893.37 77.92 C 892.02 89.24 893.14 101.60 899.86 111.18 C 902.17 114.80 905.85 117.13 909.00 119.94 C 915.68 125.39 924.44 127.86 932.98 127.78 C 943.49 127.88 953.89 125.19 963.50 121.06 C 963.49 110.12 963.52 99.18 963.49 88.24 C 955.16 88.25 946.83 88.25 938.50 88.24 C 938.50 84.92 938.50 81.60 938.49 78.28 C 950.32 78.21 962.15 78.28 973.99 78.24 C 974.01 95.16 973.99 112.07 974.00 128.99 C 962.71 135.29 949.77 137.93 937.03 139.20 C 923.54 139.48 909.45 136.02 898.95 127.21 C 888.69 118.78 882.70 105.95 880.94 92.95 C 879.59 77.13 883.61 60.28 894.27 48.18 Z" />
<path fill="#790008" opacity="1.00" d=" M 992.04 137.11 C 1005.07 101.68 1018.04 66.23 1030.97 30.76 C 1036.17 30.73 1041.37 30.79 1046.58 30.71 C 1058.53 64.36 1070.21 98.11 1082.12 131.78 C 1082.68 133.29 1082.99 134.87 1083.12 136.49 C 1078.93 136.51 1074.76 136.49 1070.59 136.51 C 1067.20 127.11 1063.90 117.67 1060.56 108.26 C 1045.82 108.24 1031.08 108.23 1016.35 108.27 C 1013.19 117.94 1009.91 127.57 1006.73 137.23 C 1001.83 137.20 996.93 137.39 992.04 137.11 M 1038.58 44.27 C 1032.78 61.38 1026.73 78.40 1020.98 95.53 C 1032.63 95.47 1044.29 95.51 1055.94 95.51 C 1050.27 78.39 1044.39 61.34 1038.58 44.27 Z" />
</g>
<g id="#ffffffff">
<path fill="#ffffff" opacity="1.00" d=" M 22.01 31.02 C 51.38 30.97 80.75 31.02 110.12 31.00 C 123.05 48.17 136.06 65.30 149.01 82.47 C 119.62 82.53 90.23 82.50 60.84 82.48 C 47.92 65.31 34.87 48.24 22.01 31.02 Z" />
</g>
<g id="#ed3024ff">
<path fill="#ed3024" opacity="1.00" d=" M 60.84 82.48 C 90.23 82.50 119.62 82.53 149.01 82.47 L 149.26 82.46 C 135.97 99.48 123.25 116.93 110.05 134.01 C 80.72 133.95 51.40 134.07 22.07 133.95 C 34.90 116.72 48.03 99.72 60.84 82.48 Z" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.2 KiB

View File

@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="80" height="24" viewBox="0 0 80 24">
<rect width="80" height="24" rx="4" fill="#0B63A9"/>
<text x="40" y="16" text-anchor="middle" font-family="Arial, Helvetica, sans-serif" font-size="12" fill="#ffffff">BCA</text>
</svg>

Before

Width:  |  Height:  |  Size: 272 B

View File

@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="80" height="24" viewBox="0 0 80 24">
<rect width="80" height="24" rx="4" fill="#F36E21"/>
<text x="40" y="16" text-anchor="middle" font-family="Arial, Helvetica, sans-serif" font-size="12" fill="#ffffff">BNI</text>
</svg>

Before

Width:  |  Height:  |  Size: 272 B

View File

@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="80" height="24" viewBox="0 0 80 24">
<rect width="80" height="24" rx="4" fill="#003A8E"/>
<text x="40" y="16" text-anchor="middle" font-family="Arial, Helvetica, sans-serif" font-size="12" fill="#ffffff">BRI</text>
</svg>

Before

Width:  |  Height:  |  Size: 272 B

View File

@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="80" height="24" viewBox="0 0 80 24">
<rect width="80" height="24" rx="4" fill="#C10E1A"/>
<text x="40" y="16" text-anchor="middle" font-family="Arial, Helvetica, sans-serif" font-size="12" fill="#ffffff">CIMB</text>
</svg>

Before

Width:  |  Height:  |  Size: 273 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="80" height="24" viewBox="0 0 80 24">
<rect width="80" height="24" rx="4" fill="#1E2A4A"/>
<text x="40" y="16" text-anchor="middle" font-family="Arial, Helvetica, sans-serif" font-size="12" fill="#ffffff">Mandiri</text>
</svg>

Before

Width:  |  Height:  |  Size: 276 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

View File

@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="80" height="24" viewBox="0 0 80 24">
<rect width="80" height="24" rx="4" fill="#009B4C"/>
<text x="40" y="16" text-anchor="middle" font-family="Arial, Helvetica, sans-serif" font-size="12" fill="#ffffff">Permata</text>
</svg>

Before

Width:  |  Height:  |  Size: 276 B

View File

@ -4,9 +4,10 @@ import React from 'react'
import { PaymentInstructions } from './PaymentInstructions'
import { BcaInstructionList } from './BcaInstructionList'
import { TrustStrip } from './TrustStrip'
import { BankLogosRow, BankLogo, type BankKey } from './PaymentLogos'
import { type BankKey } from './PaymentLogos'
import { postCharge } from '../../../services/api'
import { Alert } from '../../../components/alert/Alert'
import { InlinePaymentStatus } from './InlinePaymentStatus'
// Global guard to prevent duplicate auto-charge across StrictMode double-mounts
const attemptedChargeKeys = new Set<string>()
@ -137,11 +138,9 @@ export function BankTransferPanel({ orderId, amount, locked, onChargeInitiated,
return (
<div className="space-y-3">
<div className="font-medium">Transfer bank</div>
<BankLogosRow compact />
{selected && (
<div className="flex items-center gap-2 text-base">
<span className="text-black/60 dark:text-white/60">Bank:</span>
<BankLogo bank={selected} compact />
<span className="text-black/80 dark:text-white/80 font-semibold">{selected.toUpperCase()}</span>
</div>
)}
@ -155,7 +154,10 @@ export function BankTransferPanel({ orderId, amount, locked, onChargeInitiated,
<div className="text-sm font-medium mb-2">Virtual Account</div>
<div className="text-sm text-black/70 dark:text-white/70">
{vaCode ? (
<span>Nomor VA: <span className="font-mono text-2xl md:text-3xl font-semibold tracking-wider text-black dark:text-white">{vaCode}</span></span>
<span>
Nomor VA:
<span className="block break-all mt-1 font-mono text-xl sm:text-2xl md:text-3xl font-semibold tracking-normal text-black dark:text-white">{vaCode}</span>
</span>
) : (
<span className="inline-flex items-center gap-2" role="status" aria-live="polite">
{busy && <span className="h-3 w-3 animate-spin rounded-full border-2 border-black/40 dark:border-white/40 border-t-transparent" aria-hidden />}
@ -176,6 +178,10 @@ export function BankTransferPanel({ orderId, amount, locked, onChargeInitiated,
</div>
</div>
)}
{/* Status inline dengan polling otomatis */}
{selected && (
<InlinePaymentStatus orderId={orderId} method="bank_transfer" compact />
)}
{selected && (
<div className="pt-2">
{selected === 'bca' ? (
@ -281,12 +287,12 @@ export function BankTransferPanel({ orderId, amount, locked, onChargeInitiated,
</Button>
)}
<Button
variant="secondary"
variant="outline"
className="w-full"
disabled={busy || (!locked && !vaCode && !billKey)}
onClick={() => nav.toStatus(orderId, 'bank_transfer')}
>
Cek Status Pembayaran
Buka halaman status
</Button>
</div>
<TrustStrip />

View File

@ -4,6 +4,7 @@ import React from 'react'
import { PaymentInstructions } from './PaymentInstructions'
import { TrustStrip } from './TrustStrip'
import { postCharge } from '../../../services/api'
import { InlinePaymentStatus } from './InlinePaymentStatus'
type StoreKey = 'alfamart' | 'indomaret'
@ -115,13 +116,14 @@ export function CStorePanel({ orderId, amount, locked, onChargeInitiated, defaul
<div className="mt-2"><Button variant="outline" className="w-full sm:w-auto" onClick={() => copy(paymentCode, 'Kode pembayaran')} disabled={!paymentCode || busy}>Copy Kode</Button></div>
</div>
<Button
variant="secondary"
variant="outline"
className="w-full"
disabled={busy || (!locked && !paymentCode)}
onClick={() => nav.toStatus(orderId, 'cstore')}
>
Cek Status Pembayaran
Buka halaman status
</Button>
<InlinePaymentStatus orderId={orderId} method="cstore" />
</div>
<TrustStrip />
</div>

View File

@ -8,6 +8,7 @@ import { ensureMidtrans3ds, getCardToken, authenticate3ds } from '../lib/midtran
import { Logger } from '../../../lib/logger'
import { Env } from '../../../lib/env'
import { postCharge } from '../../../services/api'
import { InlinePaymentStatus } from './InlinePaymentStatus'
export function CardPanel({ orderId, amount, locked, onChargeInitiated }: { orderId: string; amount: number; locked?: boolean; onChargeInitiated?: () => void }) {
const nav = usePaymentNavigation()
@ -145,14 +146,16 @@ export function CardPanel({ orderId, amount, locked, onChargeInitiated }: { orde
) : 'Bayar sekarang'}
</Button>
<Button
variant="secondary"
variant="outline"
className="w-full"
disabled={busy || (!charged && !locked)}
onClick={() => nav.toStatus(orderId, 'credit_card')}
>
Cek Status Pembayaran
Buka halaman status
</Button>
</div>
{/* Status inline dengan polling otomatis */}
<InlinePaymentStatus orderId={orderId} method="credit_card" compact />
<TrustStrip />
</div>
)

View File

@ -5,6 +5,7 @@ import { PaymentInstructions } from './PaymentInstructions'
import { TrustStrip } from './TrustStrip'
import { GoPayLogosRow } from './PaymentLogos'
import { postCharge } from '../../../services/api'
import { InlinePaymentStatus } from './InlinePaymentStatus'
// Global guards/tasks to stabilize QR generation across StrictMode remounts
const attemptedChargeKeys = new Set<string>()
@ -135,20 +136,23 @@ export function GoPayPanel({ orderId, amount, locked, onChargeInitiated }: { ord
<div className="text-xs text-black/60 dark:text-white/60">Metode terkunci. Gunakan QR/deeplink untuk menyelesaikan pembayaran.</div>
)}
<div className="pt-2">
<Button
variant="secondary"
className="w-full"
aria-busy={busy}
disabled={busy || (!locked && actions.length === 0)}
onClick={() => { setBusy(true); onChargeInitiated?.(); setTimeout(() => { nav.toStatus(orderId, mode) ; setBusy(false) }, 250) }}
>
{busy ? (
<span className="inline-flex items-center justify-center gap-2" role="status" aria-live="polite">
<span className="h-4 w-4 animate-spin rounded-full border-2 border-black/40 dark:border-white/60 border-t-transparent" aria-hidden />
Menuju status
</span>
) : 'Cek Status Pembayaran'}
</Button>
<InlinePaymentStatus orderId={orderId} method={mode} />
<div className="mt-2">
<Button
variant="outline"
className="w-full"
aria-busy={busy}
disabled={busy || (!locked && actions.length === 0)}
onClick={() => { setBusy(true); onChargeInitiated?.(); setTimeout(() => { nav.toStatus(orderId, mode) ; setBusy(false) }, 250) }}
>
{busy ? (
<span className="inline-flex items-center justify-center gap-2" role="status" aria-live="polite">
<span className="h-4 w-4 animate-spin rounded-full border-2 border-black/40 dark:border-white/60 border-t-transparent" aria-hidden />
Menuju status
</span>
) : 'Buka halaman status'}
</Button>
</div>
</div>
<TrustStrip />
</div>

View File

@ -0,0 +1,102 @@
import React from 'react'
import { Button } from '../../../components/ui/button'
import { usePaymentNavigation } from '../lib/navigation'
import { usePaymentStatus } from '../lib/usePaymentStatus'
import type { PaymentStatusResponse } from '../lib/midtrans'
function formatIDR(amount?: string) {
if (!amount) return ''
const n = Number(amount)
if (Number.isNaN(n)) return amount
return new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', maximumFractionDigits: 0 }).format(Math.round(n))
}
export function InlinePaymentStatus({ orderId, method, compact }: { orderId: string; method?: string; compact?: boolean }) {
const nav = usePaymentNavigation()
const { data, isLoading, error, refetch, isRefetching } = usePaymentStatus(orderId)
const status = (data?.status ?? 'pending') as PaymentStatusResponse['status']
const isFinal = ['settlement', 'capture', 'expire', 'cancel', 'deny', 'refund', 'chargeback'].includes(status)
const isSuccess = status === 'settlement' || status === 'capture'
const isFailure = ['deny', 'cancel', 'expire', 'refund', 'chargeback'].includes(status)
return (
<div className={`rounded border ${compact ? 'p-2' : 'p-3'} border-black/10 dark:border-white/10 bg-white dark:bg-black/20`} aria-live="polite">
{/* Header minimal tanpa detail teknis */}
<div className="text-sm font-medium">Status pembayaran</div>
{/* Konten berdasarkan status */}
{isLoading ? (
<div className="mt-2 text-sm">
<span className="inline-flex items-center gap-2" role="status">
<span className="h-4 w-4 animate-spin rounded-full border-2 border-black/40 dark:border-white/60 border-t-transparent" aria-hidden />
Mengecek pembayaran
</span>
<div className="mt-1 text-[11px] text-black/60 dark:text-white/60">Kami memeriksa otomatis setiap 3 detik.</div>
</div>
) : error ? (
<div className="mt-2 text-sm text-brand-600">Gagal memuat status. Coba refresh.</div>
) : isSuccess ? (
<div className="mt-2">
<div className="flex items-center gap-2">
<span className="inline-flex h-6 w-6 items-center justify-center rounded-full bg-green-500/15 text-green-600">
{/* check icon */}
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" className="animate-[pop_200ms_ease-out]">
<path d="M20 6L9 17L4 12" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</span>
<div className="text-base font-semibold">Pembayaran berhasil</div>
</div>
{data?.grossAmount ? (
<div className="mt-1 text-sm text-black/70 dark:text-white/70">Total dibayar: {formatIDR(data.grossAmount)}</div>
) : null}
<div className="mt-1 text-xs text-black/60 dark:text-white/60">Terima kasih! Pesanan Anda sedang diproses.</div>
<div className="mt-3 flex flex-wrap gap-2">
<Button className="w-full sm:w-auto" onClick={() => nav.toHistory()}>Lihat riwayat pembayaran</Button>
<Button variant="outline" className="w-full sm:w-auto" onClick={() => nav.toCheckout()}>Kembali ke checkout</Button>
<Button variant="outline" className="w-full sm:w-auto" onClick={() => nav.toStatus(orderId, method)}>Lihat detail status</Button>
</div>
</div>
) : isFailure ? (
<div className="mt-2">
<div className="flex items-center gap-2">
<span className="inline-flex h-6 w-6 items-center justify-center rounded-full bg-red-500/15 text-red-600">
{/* x icon */}
<svg width="16" height="16" viewBox="0 0 24 24" fill="none">
<path d="M18 6L6 18" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
<path d="M6 6L18 18" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
</svg>
</span>
<div className="text-base font-semibold">Pembayaran belum berhasil</div>
</div>
<div className="mt-1 text-xs text-black/60 dark:text-white/60">Silakan coba lagi atau pilih metode lain.</div>
<div className="mt-3 flex flex-wrap gap-2">
<Button className="w-full sm:w-auto" onClick={() => nav.toCheckout()}>Coba lagi</Button>
<Button variant="outline" className="w-full sm:w-auto" onClick={() => nav.toStatus(orderId, method)}>Lihat detail status</Button>
</div>
</div>
) : (
<div className="mt-2">
<div className="flex items-center gap-2">
<span className="inline-flex h-6 w-6 items-center justify-center rounded-full bg-yellow-500/15 text-yellow-700 dark:text-yellow-300">
{/* hourglass/spinner icon */}
<span className="h-4 w-4 animate-spin rounded-full border-2 border-yellow-600/50 dark:border-yellow-300/80 border-t-transparent" aria-hidden />
</span>
<div className="text-base font-semibold">Menunggu pembayaran</div>
</div>
<div className="mt-1 text-[11px] text-black/60 dark:text-white/60">Kami memeriksa otomatis setiap 3 detik sampai selesai.</div>
<div className="mt-3 flex flex-wrap gap-2">
<Button variant="outline" className="w-full sm:w-auto" onClick={() => refetch()} aria-busy={isRefetching} disabled={isRefetching}>
{isRefetching ? (
<span className="inline-flex items-center gap-2" role="status" aria-live="polite">
<span className="h-3 w-3 animate-spin rounded-full border-2 border-black/40 dark:border-white/60 border-t-transparent" aria-hidden />
Memuat
</span>
) : 'Refresh sekarang'}
</Button>
<Button variant="outline" className="w-full sm:w-auto" onClick={() => nav.toStatus(orderId, method)}>Buka halaman status</Button>
</div>
</div>
)}
</div>
)
}

View File

@ -6,13 +6,13 @@ function toProxy(url: string) {
return `https://images.weserv.nl/?url=${encodeURIComponent(url)}`
}
function BrandImg({ src, alt, compact = false, fallbackSrc }: { src: string; alt: string; compact?: boolean; fallbackSrc?: string }) {
const sizeClass = compact ? 'h-5' : 'h-6'
function BrandImg({ src, alt, compact = false, size, fallbackSrc }: { src: string; alt: string; compact?: boolean; size?: 'xs' | 'sm' | 'md'; fallbackSrc?: string }) {
const sizeClass = size ? (size === 'xs' ? 'h-4' : size === 'sm' ? 'h-5' : 'h-6') : (compact ? 'h-5' : 'h-6')
return (
<img
src={src}
alt={alt}
className={`${sizeClass} inline-block object-contain`}
className={`${sizeClass} inline-block object-contain max-w-full`}
loading="lazy"
referrerPolicy="no-referrer"
onError={(e) => {
@ -36,31 +36,32 @@ function BrandImg({ src, alt, compact = false, fallbackSrc }: { src: string; alt
}
const BANK_LOGOS: Record<BankKey, { alt: string; src: string; fb: string }> = {
bca: { alt: 'BCA', src: '/logos/bca.svg', fb: 'https://upload.wikimedia.org/wikipedia/commons/3/3f/Bank_Central_Asia.svg' },
bni: { alt: 'BNI', src: '/logos/bni.svg', fb: 'https://upload.wikimedia.org/wikipedia/commons/2/23/Bank_Negara_Indonesia_logo.svg' },
bri: { alt: 'BRI', src: '/logos/bri.svg', fb: 'https://upload.wikimedia.org/wikipedia/commons/4/42/BRI_2020.svg' },
cimb: { alt: 'CIMB Niaga', src: '/logos/cimb.svg', fb: 'https://upload.wikimedia.org/wikipedia/commons/2/2d/CIMB_Niaga_logo.svg' },
mandiri: { alt: 'Bank Mandiri', src: '/logos/mandiri.svg', fb: 'https://upload.wikimedia.org/wikipedia/commons/d/df/Bank_Mandiri_logo_2016.svg' },
permata: { alt: 'PermataBank', src: '/logos/permata.svg', fb: 'https://upload.wikimedia.org/wikipedia/commons/4/42/PermataBank_logo.svg' },
// Sumber lokal mengikuti file yang tersedia di public/logos/
bca: { alt: 'BCA', src: '/logos/1199px-Bank_Central_Asia.svg.png', fb: 'https://upload.wikimedia.org/wikipedia/commons/3/3f/Bank_Central_Asia.svg' },
bni: { alt: 'BNI', src: '/logos/Bank_Negara_Indonesia_logo_(2004).svg', fb: 'https://upload.wikimedia.org/wikipedia/commons/2/23/Bank_Negara_Indonesia_logo.svg' },
bri: { alt: 'BRI', src: '/logos/BRI_2025.png', fb: 'https://upload.wikimedia.org/wikipedia/commons/4/42/BRI_2020.svg' },
cimb: { alt: 'CIMB Niaga', src: '/logos/CIMB_Niaga_logo.svg', fb: 'https://upload.wikimedia.org/wikipedia/commons/2/2d/CIMB_Niaga_logo.svg' },
mandiri: { alt: 'Bank Mandiri', src: '/logos/Bank_Mandiri_logo_2016.svg', fb: 'https://upload.wikimedia.org/wikipedia/commons/d/df/Bank_Mandiri_logo_2016.svg' },
permata: { alt: 'PermataBank', src: '/logos/permata Bank.png', fb: 'https://upload.wikimedia.org/wikipedia/commons/4/42/PermataBank_logo.svg' },
}
export function BankLogo({ bank, compact = false }: { bank: BankKey; compact?: boolean }) {
export function BankLogo({ bank, compact = false, size }: { bank: BankKey; compact?: boolean; size?: 'xs' | 'sm' | 'md' }) {
const cfg = BANK_LOGOS[bank]
return <BrandImg src={cfg.src} alt={cfg.alt} compact={compact} fallbackSrc={cfg.fb} />
return <BrandImg src={cfg.src} alt={cfg.alt} compact={compact} size={size} fallbackSrc={cfg.fb} />
}
export function BankLogosRow({ compact = false }: { compact?: boolean }) {
export function BankLogosRow({ compact = false, size }: { compact?: boolean; size?: 'xs' | 'sm' | 'md' }) {
const all: BankKey[] = ['bca', 'bni', 'bri', 'cimb', 'mandiri', 'permata']
return (
<div className="flex items-center gap-2">
{all.map((k) => (
<BankLogo key={k} bank={k} compact={compact} />
<BankLogo key={k} bank={k} compact={compact} size={size} />
))}
</div>
)
}
export function CardLogosRow({ compact = false }: { compact?: boolean }) {
export function CardLogosRow({ compact = false, size }: { compact?: boolean; size?: 'xs' | 'sm' | 'md' }) {
const logos = [
{ alt: 'Visa', src: '/logos/visa.svg', fb: 'https://upload.wikimedia.org/wikipedia/commons/0/04/Visa.svg' },
{ alt: 'Mastercard', src: '/logos/mastercard.svg', fb: 'https://upload.wikimedia.org/wikipedia/commons/2/2a/Mastercard-logo.svg' },
@ -70,25 +71,48 @@ export function CardLogosRow({ compact = false }: { compact?: boolean }) {
return (
<div className="flex items-center gap-2">
{logos.map((l) => (
<BrandImg key={l.alt} src={l.src} alt={l.alt} compact={compact} fallbackSrc={l.fb} />
<BrandImg key={l.alt} src={l.src} alt={l.alt} compact={compact} size={size} fallbackSrc={l.fb} />
))}
</div>
)
}
export function LogoGoPay({ compact = false }: { compact?: boolean }) {
return <BrandImg src="/logos/gopay.svg" fallbackSrc="https://upload.wikimedia.org/wikipedia/commons/1/1e/Logo_GoPay.svg" alt="GoPay" compact={compact} />
export function LogoGoPay({ compact = false, size }: { compact?: boolean; size?: 'xs' | 'sm' | 'md' }) {
// Gunakan file lokal yang tersedia di public/logos/
return <BrandImg src="/logos/Gopay_logo.svg" fallbackSrc="https://upload.wikimedia.org/wikipedia/commons/1/1e/Logo_GoPay.svg" alt="GoPay" compact={compact} size={size} />
}
export function LogoQRIS({ compact = false }: { compact?: boolean }) {
return <BrandImg src="/logos/qris.svg" fallbackSrc="https://upload.wikimedia.org/wikipedia/commons/0/0a/QRIS_Logo.svg" alt="QRIS" compact={compact} />
export function LogoQRIS({ compact = false, size }: { compact?: boolean; size?: 'xs' | 'sm' | 'md' }) {
// Gunakan file lokal yang tersedia di public/logos/
return <BrandImg src="/logos/Logo_QRIS.svg" fallbackSrc="https://upload.wikimedia.org/wikipedia/commons/0/0a/QRIS_Logo.svg" alt="QRIS" compact={compact} size={size} />
}
export function GoPayLogosRow({ compact = false }: { compact?: boolean }) {
export function LogoAlfamart({ compact = false, size }: { compact?: boolean; size?: 'xs' | 'sm' | 'md' }) {
return <BrandImg src="/logos/ALFAMART_LOGO_BARU.png" alt="Alfamart" compact={compact} size={size} />
}
export function LogoIndomaret({ compact = false, size }: { compact?: boolean; size?: 'xs' | 'sm' | 'md' }) {
return <BrandImg src="/logos/Logo_Indomaret.png" alt="Indomaret" compact={compact} size={size} />
}
export function LogoCpay({ compact = false, size }: { compact?: boolean; size?: 'xs' | 'sm' | 'md' }) {
// Sumber lokal sesuai permintaan: public/logos/Cifo_cpay.png
return <BrandImg src="/logos/Cifo_cpay.png" alt="cPay" compact={compact} size={size} />
}
export function CStoreLogosRow({ compact = false, size }: { compact?: boolean; size?: 'xs' | 'sm' | 'md' }) {
return (
<div className="flex items-center gap-2">
<LogoGoPay compact={compact} />
<LogoQRIS compact={compact} />
<LogoAlfamart compact={compact} size={size} />
<LogoIndomaret compact={compact} size={size} />
</div>
)
}
export function GoPayLogosRow({ compact = false, size }: { compact?: boolean; size?: 'xs' | 'sm' | 'md' }) {
return (
<div className="flex items-center gap-2">
<LogoGoPay compact={compact} size={size} />
<LogoQRIS compact={compact} size={size} />
</div>
)
}

View File

@ -1,8 +1,8 @@
import React from 'react'
import { BankLogosRow, CardLogosRow, GoPayLogosRow } from './PaymentLogos'
import { CardLogosRow, GoPayLogosRow, CStoreLogosRow, LogoCpay } from './PaymentLogos'
import { Env } from '../../../lib/env'
export type PaymentMethod = 'bank_transfer' | 'credit_card' | 'gopay' | 'cstore'
export type PaymentMethod = 'bank_transfer' | 'credit_card' | 'gopay' | 'cstore' | 'cpay'
export interface PaymentMethodListProps {
selected?: PaymentMethod
@ -13,18 +13,20 @@ export interface PaymentMethodListProps {
}
const baseItems: Array<{ key: PaymentMethod; title: string; subtitle: string; icon?: React.ReactNode }> = [
{ key: 'bank_transfer', title: 'Transfer bank', subtitle: 'BCA • BNI • BRI • CIMB • Mandiri • Permata', icon: <BankLogosRow compact /> },
{ key: 'credit_card', title: 'Kartu kredit/debit', subtitle: 'Visa • MasterCard • JCB • Amex', icon: <CardLogosRow compact /> },
{ key: 'gopay', title: 'Gopay/QRIS', subtitle: 'Scan & bayar via QR', icon: <GoPayLogosRow compact /> },
{ key: 'cstore', title: 'Convenience Store', subtitle: 'Alfamart • Indomaret' },
{ key: 'bank_transfer', title: 'Transfer bank', subtitle: 'BCA • BNI • BRI • CIMB • Mandiri • Permata', icon: <img src="/logos/logo-semua-bank.PNG" alt="Semua bank yang didukung" className="h-6 sm:h-8 object-contain" /> },
{ key: 'credit_card', title: 'Kartu kredit/debit', subtitle: 'Visa • MasterCard • JCB • Amex', icon: <CardLogosRow compact size="xs" /> },
{ key: 'gopay', title: 'Gopay/QRIS', subtitle: 'Scan & bayar via QR', icon: <GoPayLogosRow compact size="xs" /> },
{ key: 'cstore', title: 'Convenience Store', subtitle: '', icon: <CStoreLogosRow compact size="xs" /> },
{ key: 'cpay', title: 'cPay', subtitle: 'Bayar via aplikasi CIFO Token', icon: <LogoCpay compact size="md" /> },
]
export function PaymentMethodList({ selected, onSelect, renderPanel, disabled, enabled }: PaymentMethodListProps) {
const enabledMap: Record<PaymentMethod, boolean> = enabled ?? {
bank_transfer: Env.ENABLE_BANK_TRANSFER,
credit_card: Env.ENABLE_CREDIT_CARD,
gopay: Env.ENABLE_GOPAY,
cstore: Env.ENABLE_CSTORE,
const enabledMap: Record<PaymentMethod, boolean> = {
bank_transfer: enabled?.bank_transfer ?? Env.ENABLE_BANK_TRANSFER,
credit_card: enabled?.credit_card ?? Env.ENABLE_CREDIT_CARD,
gopay: enabled?.gopay ?? Env.ENABLE_GOPAY,
cstore: enabled?.cstore ?? Env.ENABLE_CSTORE,
cpay: enabled?.cpay ?? Env.ENABLE_CPAY,
}
const items = baseItems.filter((it) => enabledMap[it.key])
return (
@ -41,14 +43,25 @@ export function PaymentMethodList({ selected, onSelect, renderPanel, disabled, e
aria-expanded={selected === it.key}
aria-controls={`panel-${it.key}`}
>
<div>
<div className="flex-1">
<div className="text-base font-semibold text-black dark:text-white">{it.title}</div>
<div className="text-sm text-black/70 dark:text-white/70">{it.subtitle}</div>
{it.key === 'bank_transfer' && it.subtitle && (
<div className="mt-1 text-xs text-black/60 dark:text-white/60">
{it.subtitle}
</div>
)}
{it.key === 'cpay' && it.subtitle && (
<div className="mt-1 text-xs text-black/60 dark:text-white/60">
{it.subtitle}
</div>
)}
</div>
<div className="flex items-center gap-2">
<span className="hidden sm:block" aria-hidden>
{it.icon}
</span>
{it.icon && (
<span aria-hidden>
{it.icon}
</span>
)}
<span aria-hidden className={`text-black/60 dark:text-white/60 text-lg transition-transform ${selected === it.key ? 'rotate-90' : ''}`}></span>
</div>
</button>

View File

@ -25,9 +25,10 @@ export interface PaymentSheetProps {
amount: number
expireAt: number // epoch ms
children?: React.ReactNode
showStatusCTA?: boolean
}
export function PaymentSheet({ merchantName = 'Zara', orderId, amount, expireAt, children }: PaymentSheetProps) {
export function PaymentSheet({ merchantName = 'Zara', orderId, amount, expireAt, children, showStatusCTA = true }: PaymentSheetProps) {
const countdown = useCountdown(expireAt)
const [expanded, setExpanded] = React.useState(true)
return (
@ -76,15 +77,17 @@ export function PaymentSheet({ merchantName = 'Zara', orderId, amount, expireAt,
<TrustStrip location="sheet" />
</div>
{/* Sticky CTA (mobile-friendly) */}
<div className="sticky bottom-0 bg-white/95 dark:bg-neutral-900/95 backdrop-blur border-t border-black/10 dark:border-white/10 p-3 pb-[env(safe-area-inset-bottom)]">
<Link
to={`/payments/${orderId}/status`}
aria-label="Buka halaman status pembayaran"
className="w-full block text-center rounded bg-[#0c1f3f] text-white py-3 text-base font-semibold hover:bg-[#0a1a35] focus:outline-none focus-visible:ring-3 focus-visible:ring-offset-2 focus-visible:ring-[#0c1f3f]"
>
Cek status pembayaran
</Link>
</div>
{showStatusCTA && (
<div className="sticky bottom-0 bg-white/95 dark:bg-neutral-900/95 backdrop-blur border-t border-black/10 dark:border-white/10 p-3 pb-[env(safe-area-inset-bottom)]">
<Link
to={`/payments/${orderId}/status`}
aria-label="Buka halaman status pembayaran"
className="w-full block text-center rounded bg-[#0c1f3f] text-white py-3 text-base font-semibold hover:bg-[#0a1a35] focus:outline-none focus-visible:ring-3 focus-visible:ring-offset-2 focus-visible:ring-[#0c1f3f]"
>
Cek status pembayaran
</Link>
</div>
)}
</div>
</div>
)

View File

@ -8,7 +8,7 @@ import { BankTransferPanel } from '../features/payments/components/BankTransferP
import { CardPanel } from '../features/payments/components/CardPanel'
import { GoPayPanel } from '../features/payments/components/GoPayPanel'
import { CStorePanel } from '../features/payments/components/CStorePanel'
import { BankLogo, type BankKey } from '../features/payments/components/PaymentLogos'
import { BankLogo, type BankKey, LogoAlfamart, LogoIndomaret } from '../features/payments/components/PaymentLogos'
import { usePaymentConfig } from '../features/payments/lib/usePaymentConfig'
import { Logger } from '../lib/logger'
import React from 'react'
@ -72,7 +72,7 @@ export function CheckoutPage() {
</Alert>
)}
<PaymentSheet merchantName="Zara" orderId={orderId} amount={amount} expireAt={expireAt}>
<PaymentSheet merchantName="Zara" orderId={orderId} amount={amount} expireAt={expireAt} showStatusCTA={currentStep === 3}>
{/* Wizard 3 langkah: Step 1 (Form Dummy) → Step 2 (Pilih Metode) → Step 3 (Panel Metode) */}
{currentStep === 1 && (
<div className="space-y-3">
@ -142,6 +142,15 @@ export function CheckoutPage() {
setSelected(m)
if (m === 'bank_transfer' || m === 'cstore') {
// Panel akan tampil di bawah item menggunakan renderPanel
} else if (m === 'cpay') {
// Redirect ke aplikasi cPay (CIFO Token) di Play Store
try {
Logger.info('cpay.redirect.start')
window.open('https://play.google.com/store/apps/details?id=com.cifo.walanja', '_blank')
Logger.info('cpay.redirect.done')
} catch (e) {
Logger.error('cpay.redirect.error', { message: (e as Error)?.message })
}
} else {
setIsBusy(true)
setTimeout(() => { setCurrentStep(3); setIsBusy(false) }, 300)
@ -161,6 +170,13 @@ export function CheckoutPage() {
if (m === 'bank_transfer') {
return (
<div className="space-y-2" aria-live="polite">
<div className="flex justify-center">
<img
src="/logos/logo-semua-bank.PNG"
alt="Logo semua bank yang didukung"
className="max-h-20 sm:max-h-24 object-contain"
/>
</div>
<div className="text-xs text-black/60 dark:text-white/60">Pilih bank untuk membuat Virtual Account</div>
<div className={`grid grid-cols-3 gap-2 ${isBusy ? 'pointer-events-none opacity-60' : ''}`}>
{(['bca','bni','bri','cimb','mandiri','permata'] as BankKey[]).map((bk) => (
@ -172,11 +188,10 @@ export function CheckoutPage() {
setIsBusy(true)
setTimeout(() => { setCurrentStep(3); setIsBusy(false) }, 300)
}}
className="rounded border border-black/10 dark:border-white/10 bg-white dark:bg-black/20 p-2 flex flex-col items-center gap-1 hover:bg-black/5 dark:hover:bg-white/10"
className="rounded border border-black/10 dark:border-white/10 bg-white dark:bg-black/20 p-2 flex items-center justify-center overflow-hidden hover:bg-black/5 dark:hover:bg-white/10"
aria-label={`Pilih bank ${bk.toUpperCase()}`}
>
<BankLogo bank={bk} />
<span className="text-xs text-black/70 dark:text-white/70">{bk.toUpperCase()}</span>
</button>
))}
</div>
@ -203,10 +218,10 @@ export function CheckoutPage() {
setIsBusy(true)
setTimeout(() => { setCurrentStep(3); setIsBusy(false) }, 300)
}}
className="rounded border border-black/10 dark:border-white/10 bg-white dark:bg-black/20 p-2 flex flex-col items-center gap-1 hover:bg-black/5 dark:hover:bg-white/10"
className="rounded border border-black/10 dark:border-white/10 bg-white dark:bg-black/20 p-2 flex items-center justify-center overflow-hidden hover:bg-black/5 dark:hover:bg-white/10"
aria-label={`Pilih toko ${st.toUpperCase()}`}
>
<span className="text-sm font-medium">{st.toUpperCase()}</span>
{st === 'alfamart' ? <LogoAlfamart /> : <LogoIndomaret />}
</button>
))}
</div>
@ -255,5 +270,6 @@ function defaultEnabled(): Record<PaymentMethod, boolean> {
credit_card: Env.ENABLE_CREDIT_CARD,
gopay: Env.ENABLE_GOPAY,
cstore: Env.ENABLE_CSTORE,
cpay: Env.ENABLE_CPAY,
}
}