Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# KiriminAja Official — WordPress/WooCommerce Shipping Plugin

These are local repository instructions for GitHub Copilot. `AGENTS.md` is the canonical source for shared agentic workflow rules. Keep this file aligned with `AGENTS.md` and `CLAUDE.md`.

## Project Overview

KiriminAja Official is a WooCommerce shipping plugin for Indonesian online sellers. It provides real-time shipping rates from multiple couriers, COD support, pickup scheduling, label printing, and package tracking—all from the WooCommerce dashboard.
Expand Down Expand Up @@ -254,6 +256,16 @@ Tables are created via `Migration\SetupMigration::register()` which calls `setti

---

## Local Workflow Rules

- Use `rtk` before shell commands in this workspace.
- Check `git status --short --branch` before edits and avoid unrelated user changes.
- Prefer narrow fixes in the existing service/controller/template structure.
- Follow WordPress Coding Standards unless nearby code has a stronger local convention.
- When using `$wpdb->prepare()` with dynamic `IN (...)` placeholders, pass the prepared SQL directly into `$wpdb->query()` or similar calls. Do not store prepared SQL in an intermediate variable. Keep replacement counts exact and avoid interpolated placeholder variables that trigger WordPressCS / Plugin Check `PreparedSQL` warnings.
- Do not start local WordPress or dev servers unless explicitly requested.
- When changing packaged source files, run `make zip` before final verification.

## Testing

- Test runner: ParaTest
Expand Down
3 changes: 3 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# AGENTS.md

This is the canonical local instruction file for agentic coding tools in this repository. Keep `AGENTS.md`, `CLAUDE.md`, and `.github/copilot-instructions.md` aligned when these project rules change.

## Project Context

This repository is the KiriminAja Official WordPress plugin for WooCommerce. Keep changes scoped to plugin behavior, tests, packaging, and documentation that directly support the requested task.
Expand All @@ -11,6 +13,7 @@ This repository is the KiriminAja Official WordPress plugin for WooCommerce. Kee
- Prefer narrow fixes in the existing service/controller/template structure.
- Follow the official WordPress Coding Standards for all plugin changes: https://developer.wordpress.org/coding-standards/wordpress-coding-standards/
- Apply those standards to PHP, JavaScript, CSS, HTML, documentation, escaping, sanitization, naming, and formatting decisions unless the surrounding file has a stronger local convention.
- Be extra careful with WordPressCS / Plugin Check database sniffs: when using `$wpdb->prepare()` with dynamic `IN (...)` placeholders, pass the prepared SQL directly into the query call instead of storing it in an intermediate variable, and avoid mismatched replacement counts or interpolated placeholder variables that trigger `PreparedSQL` warnings.
- Do not start local WordPress or dev servers unless explicitly requested.
- When changing packaged source files, run `make zip` before final verification because build/source parity is tested.

Expand Down
29 changes: 29 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# CLAUDE.md

This is the canonical local instruction file for Claude and other agentic coding tools in this repository. Keep `CLAUDE.md`, `AGENTS.md`, and `.github/copilot-instructions.md` aligned when these project rules change.

## Project Context

This repository is the KiriminAja Official WordPress plugin for WooCommerce. Keep changes scoped to plugin behavior, tests, packaging, and documentation that directly support the requested task.

## Workflow

- Use `rtk` before shell commands in this workspace.
- Check `git status --short --branch` before edits and avoid touching unrelated user changes.
- Prefer narrow fixes in the existing service/controller/template structure.
- Follow the official WordPress Coding Standards for all plugin changes: https://developer.wordpress.org/coding-standards/wordpress-coding-standards/
- Apply those standards to PHP, JavaScript, CSS, HTML, documentation, escaping, sanitization, naming, and formatting decisions unless the surrounding file has a stronger local convention.
- Be extra careful with WordPressCS / Plugin Check database sniffs: when using `$wpdb->prepare()` with dynamic `IN (...)` placeholders, pass the prepared SQL directly into the query call instead of storing it in an intermediate variable, and avoid mismatched replacement counts or interpolated placeholder variables that trigger `PreparedSQL` warnings.
- Do not start local WordPress or dev servers unless explicitly requested.
- When changing packaged source files, run `make zip` before final verification because build/source parity is tested.

## Testing

- Use ParaTest as the default test runner: `make test`.
- For focused checks, run `vendor/bin/paratest --configuration paratest.xml --filter <Name>`.
- Keep `tests/` string/structure assertions aligned with the current implementation when behavior is intentionally changed.

## Packaging

- `CLAUDE.md`, `AGENTS.md`, `paratest.xml`, tests, development files, and cache directories must not be included in the distributable zip.
- The distributable archive is produced by `make zip`.
5 changes: 5 additions & 0 deletions inc/Controllers/TransactionProcessController.php
Original file line number Diff line number Diff line change
Expand Up @@ -712,6 +712,11 @@ public function renderWooActionModalTemplatesForKiriofPage()
<pin-input id="kiriof-pin-widget" class="kiriof-pin-widget" length="6" pattern="[0-9]" autocomplete="one-time-code" inputmode="numeric" mask aria-label="<?php esc_attr_e('Enter 6-digit PIN', 'kiriminaja-official'); ?>"></pin-input>
<input type="password" id="kiriof-pin-fallback" class="kiriof-pin-fallback" maxlength="6" pattern="[0-9]{6}" inputmode="numeric" placeholder="------" autocomplete="one-time-code" style="display:none;">
<input type="hidden" id="kiriof-pin-input" name="pin" value="">
<label for="kiriof-pin-remember" class="kiriof-pin-remember" style="display:flex;gap:8px;align-items:flex-start;margin:12px 0 0;">
<input type="checkbox" id="kiriof-pin-remember" name="remember_pin" value="1">
<span><?php echo esc_html($kiriof_pin_cache_label); ?></span>
</label>
<p class="kiriof-pin-cache-notice" style="display:none;font-size:12px;color:#2271b1;margin:8px 0 0;"></p>
<p class="kiriof-pin-error err_msg" style="display:none;"></p>
</div>
</div>
Expand Down
16 changes: 16 additions & 0 deletions lang/kiriminaja-official-id_ID.po
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,9 @@ msgstr "Jadwal Tersedia"
msgid "-- Select schedule --"
msgstr "-- Pilih jadwal --"

msgid "Browser secure storage is unavailable. PIN will not be remembered."
msgstr "Penyimpanan aman browser tidak tersedia. PIN tidak akan diingat."

msgid "Close"
msgstr "Tutup"

Expand All @@ -263,6 +266,19 @@ msgstr "Tidak ada jadwal pickup yang tersedia."
msgid "Please select a pickup schedule."
msgstr "Silakan pilih jadwal pickup."

#, php-format
msgid "Remember PIN on this browser for %d minutes"
msgstr "Ingat PIN di browser ini selama %d menit"

msgid "Saved PIN expired. Please enter your PIN again."
msgstr "PIN tersimpan sudah kedaluwarsa. Silakan masukkan PIN Anda lagi."

msgid "Saved PIN was cleared. Please enter your latest PIN again."
msgstr "PIN tersimpan telah dihapus. Silakan masukkan PIN terbaru Anda lagi."

msgid "Saved PIN will be reused until it expires on this browser."
msgstr "PIN tersimpan akan digunakan kembali sampai masa berlakunya habis di browser ini."

#: inc/Base/Helper.php:37
#: inc/Base/Helper.php:57
msgid "Canceled"
Expand Down
189 changes: 187 additions & 2 deletions templates/transaction-process/view/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,16 @@
$kiriof_helper = kiriof_helper();
$kiriof_homeUrl = home_url();
$kiriof_adminUrl = $kiriof_homeUrl . '/wp-admin';
$kiriof_current_user = wp_get_current_user();
$kiriof_pin_cache_ttl = (int) apply_filters('kiriof_pin_cache_ttl', 15 * MINUTE_IN_SECONDS, $kiriof_current_user);
if ($kiriof_pin_cache_ttl < MINUTE_IN_SECONDS) {
$kiriof_pin_cache_ttl = MINUTE_IN_SECONDS;
}
$kiriof_pin_cache_label = sprintf(
/* translators: %d: cached PIN duration in minutes. */
__('Remember PIN on this browser for %d minutes', 'kiriminaja-official'),
(int) ceil($kiriof_pin_cache_ttl / MINUTE_IN_SECONDS)
);

/**
* @var string $locale
Expand Down Expand Up @@ -627,6 +637,16 @@
const kjPickScheduleLabel = '<?php echo esc_js(__('Pick Schedule', 'kiriminaja-official')); ?>';
const kjConfirmPinLabel = '<?php echo esc_js(__('Confirm PIN', 'kiriminaja-official')); ?>';
const kjValidateLabel = '<?php echo esc_js(__('Validate', 'kiriminaja-official')); ?>';
const kjPinCacheConfig = {
key: <?php echo wp_json_encode('kiriof_pin_cache_' . get_current_blog_id() . '_' . (int) get_current_user_id()); ?>,
ttl: <?php echo (int) $kiriof_pin_cache_ttl; ?>,
userHash: <?php echo wp_json_encode(hash_hmac('sha256', (string) get_current_user_id(), wp_salt('auth'))); ?>,
siteHash: <?php echo wp_json_encode(hash_hmac('sha256', home_url('/'), wp_salt('auth'))); ?>,
rememberLabel: <?php echo wp_json_encode(__('Saved PIN will be reused until it expires on this browser.', 'kiriminaja-official')); ?>,
expiredLabel: <?php echo wp_json_encode(__('Saved PIN expired. Please enter your PIN again.', 'kiriminaja-official')); ?>,
invalidatedLabel: <?php echo wp_json_encode(__('Saved PIN was cleared. Please enter your latest PIN again.', 'kiriminaja-official')); ?>,
unsupportedLabel: <?php echo wp_json_encode(__('Browser secure storage is unavailable. PIN will not be remembered.', 'kiriminaja-official')); ?>,
};
const kjUpdateRequestPickupCount = () => {
const pickupCount = $transactionCheckboxes().filter(':checked:not(:disabled)[data-can-pickup="1"]').length;
const printCount = $transactionCheckboxes().filter(':checked:not(:disabled)[data-can-print="1"]').length;
Expand Down Expand Up @@ -730,7 +750,7 @@ function kiriofSetModalState($modal, state) {
} else if (state==='pin' ) {
$modal.find('.kiriof-modal-state-pin').show();
$modal.find('#btn-next').text(kjValidateLabel);
kjFocusPinInput($modal);
kjPrepareCreditPinStep($modal);
} else {
$modal.find('.kiriof-modal-state-content').show();
}
Expand Down Expand Up @@ -789,6 +809,159 @@ function kjResetPinRetryState($modal) {
$modal.data('kiriofPinCooldownTimer', null);
}

function kjGetPinRememberCheckbox($modal) {
return $modal.find('#kiriof-pin-remember');
}

function kjGetPinCacheNotice($modal) {
return $modal.find('.kiriof-pin-cache-notice');
}

function kjSetPinCacheNotice($modal, message, tone) {
const $notice = kjGetPinCacheNotice($modal);
if (!message) {
$notice.hide().text('').removeAttr('data-tone');
return;
}

$notice.attr('data-tone', tone || 'info').text(message).show();
}

function kjCanUsePinCache() {
return window.isSecureContext && window.crypto && window.crypto.subtle && window.localStorage;
}

async function kjGetPinCacheKeyMaterial() {
const rawKey = await window.crypto.subtle.digest('SHA-256', new TextEncoder().encode(`${kjPinCacheConfig.userHash}:${kjPinCacheConfig.siteHash}`));
return window.crypto.subtle.importKey('raw', rawKey, 'AES-GCM', false, ['encrypt', 'decrypt']);
}

function kjGetPinCacheRecord() {
if (!kjCanUsePinCache()) {
return null;
}

try {
const rawValue = window.localStorage.getItem(kjPinCacheConfig.key);
if (!rawValue) {
return null;
}

const parsedValue = JSON.parse(rawValue);
if (!parsedValue || parsedValue.userHash !== kjPinCacheConfig.userHash || parsedValue.siteHash !== kjPinCacheConfig.siteHash) {
window.localStorage.removeItem(kjPinCacheConfig.key);
return null;
}

if (!parsedValue.expiresAt || Date.now() >= parsedValue.expiresAt) {
window.localStorage.removeItem(kjPinCacheConfig.key);
return { expired: true };
}

return parsedValue;
} catch (error) {
window.localStorage.removeItem(kjPinCacheConfig.key);
return null;
}
}

async function kjDecryptCachedPin(record) {
if (!record || !record.ciphertext || !record.iv) {
return '';
}

const key = await kjGetPinCacheKeyMaterial();
const iv = Uint8Array.from(atob(record.iv), (character) => character.charCodeAt(0));
const ciphertext = Uint8Array.from(atob(record.ciphertext), (character) => character.charCodeAt(0));
const decrypted = await window.crypto.subtle.decrypt({ name: 'AES-GCM', iv }, key, ciphertext);
return new TextDecoder().decode(decrypted).replace(/\D/g, '').substring(0, 6);
}

async function kjPersistCachedPin($modal, pin) {
if (!kjCanUsePinCache()) {
kjSetPinCacheNotice($modal, kjPinCacheConfig.unsupportedLabel, 'warning');
return false;
}

const rememberPin = kjGetPinRememberCheckbox($modal).is(':checked');
if (!rememberPin || !pin || pin.length !== 6) {
return false;
}

const key = await kjGetPinCacheKeyMaterial();
const iv = window.crypto.getRandomValues(new Uint8Array(12));
const encrypted = await window.crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, new TextEncoder().encode(pin));
const payload = {
userHash: kjPinCacheConfig.userHash,
siteHash: kjPinCacheConfig.siteHash,
expiresAt: Date.now() + (kjPinCacheConfig.ttl * 1000),
iv: btoa(String.fromCharCode(...iv)),
ciphertext: btoa(String.fromCharCode(...new Uint8Array(encrypted))),
};
window.localStorage.setItem(kjPinCacheConfig.key, JSON.stringify(payload));
kjSetPinCacheNotice($modal, kjPinCacheConfig.rememberLabel, 'success');
return true;
}

function kjClearCachedPin($modal, reason) {
if (window.localStorage) {
window.localStorage.removeItem(kjPinCacheConfig.key);
}
kjGetPinRememberCheckbox($modal).prop('checked', false);
if (reason === 'expired') {
kjSetPinCacheNotice($modal, kjPinCacheConfig.expiredLabel, 'warning');
} else if (reason === 'invalid') {
kjSetPinCacheNotice($modal, kjPinCacheConfig.invalidatedLabel, 'warning');
} else if (!reason) {
kjSetPinCacheNotice($modal, '', 'info');
}
}

async function kjRestoreCachedPin($modal) {
if (!kjCanUsePinCache()) {
return false;
}

const record = kjGetPinCacheRecord();
if (!record) {
return false;
}

if (record.expired) {
kjClearCachedPin($modal, 'expired');
return false;
}

try {
const pin = await kjDecryptCachedPin(record);
if (!pin || pin.length !== 6) {
kjClearCachedPin($modal, 'invalid');
return false;
}

kjSetPinValue($modal, pin);
kjGetPinRememberCheckbox($modal).prop('checked', true);
kjSetPinCacheNotice($modal, kjPinCacheConfig.rememberLabel, 'success');
return true;
} catch (error) {
kjClearCachedPin($modal, 'invalid');
return false;
}
}

function kjPrepareCreditPinStep($modal) {
kjEnsurePinInputReady($modal);
if (!kjCanUsePinCache()) {
kjSetPinCacheNotice($modal, kjPinCacheConfig.unsupportedLabel, 'warning');
}
window.setTimeout(function() {
void kjRestoreCachedPin($modal).finally(function() {
kjFocusPinInput($modal);
kjUpdatePickupButton($modal);
});
}, 0);
}

function kjNormalizePinErrorData($modal, data) {
const normalized = Object.assign({}, data || {});
const error = normalized.error || '';
Expand Down Expand Up @@ -839,6 +1012,9 @@ function kjNormalizePinErrorData($modal, data) {

const $modal = kiriofGetRequestPickupModal();
kjResetPinRetryState($modal);
kjSetPinValue($modal, '');
kjGetPinRememberCheckbox($modal).prop('checked', false);
kjSetPinCacheNotice($modal, '', 'info');
kiriofSetModalState($modal, 'loading');
$modal.find('#btn-next').prop('disabled', true);

Expand Down Expand Up @@ -1001,6 +1177,11 @@ function kjLoadPaymentMethodConfig($modal, totalFee) {
kjResetPinRetryState($modal);
$modal.find('#kiriof-pin-widget').removeAttr('invalid');
$modal.find('.kiriof-pin-error').hide().text('');
kjSetPinCacheNotice($modal, '', 'info');
} else {
void kjRestoreCachedPin($modal).finally(function() {
kjUpdatePickupButton($modal);
});
}
kjUpdatePickupButton($modal);
});
Expand Down Expand Up @@ -1166,6 +1347,7 @@ function kjSubmitPickup($modal, ids, schedule, paymentMethod, pin, closeModal) {
kiriofSetModalState($modal, 'content');
$errMsg.text('*<?php echo esc_js(__('Insufficient credit balance. Please top up or use QRIS.', 'kiriminaja-official')); ?>').show();
} else if (errCode === 'PIN_INVALID' || errCode === 'PIN_MAX_ATTEMPT_REACHED') {
kjClearCachedPin($modal, 'invalid');
kiriofSetModalState($modal, 'pin');
kjShowPinError($modal, resp?.data || {}, resp?.message || errCode);
} else {
Expand Down Expand Up @@ -1253,6 +1435,7 @@ function kjSubmitPickup($modal, ids, schedule, paymentMethod, pin, closeModal) {
complete: function(pinResp) {
const pinData = JSON.parse(pinResp.responseText).data;
if (pinData?.status !== 200) {
kjClearCachedPin($modal, 'invalid');
kiriofSetModalState($modal, 'pin');
const pinErr = pinData?.data;
if (pinErr?.error === 'PIN_MAX_ATTEMPT_REACHED') {
Expand All @@ -1265,7 +1448,9 @@ function kjSubmitPickup($modal, ids, schedule, paymentMethod, pin, closeModal) {
return;
}
kjResetPinRetryState($modal);
kjSubmitPickup($modal, orderIds, schedule, paymentMethod, pin, closeModal);
void kjPersistCachedPin($modal, pin).finally(function() {
kjSubmitPickup($modal, orderIds, schedule, paymentMethod, pin, closeModal);
});
}
});
return;
Expand Down
Loading
Loading