Skip to content

fix: date breaks on iOS devices set to 12-hour time, making getTokenInfo throw FormatException - #67

Merged
ZweWaiYanHtet merged 1 commit into
masterfrom
fix/ios-datetime-12hour-format
Sep 1, 2026
Merged

ZweWaiYanHtet merged 1 commit into
masterfrom
fix/ios-datetime-12hour-format

Conversation

@masatoi

@masatoi masatoi commented Aug 31, 2026

Copy link
Copy Markdown
Member

Symptom

On an iOS device set to 12-hour time (Settings > General > Date & Time > 24-Hour Time = off), passing a topup QR (check) URL to PokepayClient.getTokenInfo() fails with FormatException: Invalid date format. Payment QRs (bill) are unaffected.

Cause

APIJSONEncoder in ios/Classes/SwiftPokepaySdkPlugin.swift never set a locale on its DateFormatter, so Locale.current was used.

A fixed-format DateFormatter must specify en_US_POSIX; without it, Foundation rewrites HH into h plus an AM/PM marker while the device is on 12-hour time (Apple QA1480). The date that reaches Flutter then stops being ISO8601, and Dart's DateTime.parse throws.

pokepay-server        "2026-11-20T06:56:43.091034Z"   valid ISO8601
  ↓ ios-sdk BankAPIJSONDecoder (has en_US_POSIX)
Swift Check.expiresAt  Date                            still fine here
  ↓ flutter-sdk APIJSONEncoder (no en_US_POSIX)        ← breaks here
MethodChannel          "2026-11-20T午前6:56:43.091000Z"
  ↓ Check.fromJson → DateTime.parse
Dart                   FormatException: Invalid date format

Measured on the iOS 26.5 simulator:

Locale 24-Hour Time Before After
ja_JP on 2026-11-20T06:56:43.091000Z unchanged
ja_JP off 2026-11-20T午前6:56:43.091000Z 2026-11-20T06:56:43.091000Z
en_US off 2026-11-20T6:56:43.091000 AMZ 2026-11-20T06:56:43.091000Z
en_US on 2026-11-20T06:56:43.091000Z unchanged

Payment QRs survived because lib/responses/bill.dart has no DateTime field, so the malformed string passes straight through. lib/responses/check.dart declares expiresAt as a required DateTime.

Fix

One line in APIJSONEncoder: dateFormatter.locale = Locale(identifier: "en_US_POSIX").

APIJSONEncoder is the only place that serializes response dates — it is also the only DateFormatter in the repository. The generated AutogenMethodHandlers.swift carries no encoder of its own and routes all 150 of its call sites through self.after, so this single line fixes both the hand-written and the generated paths.

Verification

Unit (encoder in isolation): extracted APIJSONEncoder from the real plugin source, compiled it, and measured all four combinations in the table above on the iOS 26.5 simulator.

End to end: created a check and a bill on dev, then ran a minimal Flutter app (depending on this SDK by local path) on an iPhone 17 Pro / iOS 26.5.

State Device setting CHECK BILL
Before ja_JP / 12-hour FormatException: Invalid date format OK ✅
After ja_JP / 12-hour OK, expiresAt=2026-11-30 07:11:16.856Z OK ✅
After ja_JP / 24-hour OK ✅ OK ✅

The first row reproduces the reported symptom exactly. The real build resolving the Pokepay 2.2.0 pod through CocoaPods also succeeds.

Regression tests: added test/responses/check_test.dart (the first test/ in this repository). The malformed strings are the measured values above, used verbatim. flutter test passes 6/6 and flutter analyze reports No issues found!.

Impact

Before this fix, every response carrying a DateTime broke on iOS devices set to 12-hour time: Cashtray, Account (nearest_expires_at), AccountBalance, AccountCpmToken, Coupon, Message, UserTransaction.done_at on the autogen side, and others.

Already investigated

  • pokepay-server is not involved: querying the dev API directly returns 2026-11-30T07:11:16.856289Z, valid ISO8601.
  • ios-sdk needs no change: 2.2.0 already sets en_US_POSIX in both BankAPIJSONDecoder.swift and BankAPIDateFormatter.swift.
  • Android is unaffected: the Android plugin only calls res.toString() and contains no date formatting at all (zero uses of SimpleDateFormat or Locale). The behaviour where a 12/24-hour user preference rewrites a formatter is specific to Foundation/ICU.
  • Not generated code: SwiftPokepaySdkPlugin.swift carries no generated-file header, and no generate-sdk-* commit has ever touched it. No generator change is required.
  • How long this has been broken: git log -S "en_US_POSIX" returns nothing across the entire history. APIJSONEncoder has looked like this since the very first commit (209976e init lib, 2021-07-08), so this is unrelated to any recent change or to 2.2.0. The symptom took its current shape once ios-sdk fixed the decoding side in a765b19 (2022-09-05).

Note

Version, tags and CHANGELOG are deliberately untouched. Please decide the release plan separately.

Out of scope for this PR

  • CI has no test step, so the added tests do not run there yet. Let me know whether to add flutter test to the check job here or in a separate PR.
  • The build warns that pokepay_sdk does not support Swift Package Manager and that this will become an error in a future Flutter release. Unrelated to this bug, but it will need attention.

🤖 Generated with Claude Code

https://claude.ai/code/session_01W5pXxkWbYKDAnQZDpYu1RG

APIJSONEncoder の DateFormatter に locale が設定されておらず、Locale.current が
使われていた。固定フォーマットの DateFormatter に en_US_POSIX を指定しないと、
端末が12時間表示のとき Foundation が "HH" を "h" + AM/PM に書き換える (QA1480)。

その結果 Flutter チャネルに渡る日付が ISO8601 でなくなり、Dart 側の
DateTime.parse が FormatException を投げていた。

  ja_JP: 2026-11-20T午前6:56:43.091000Z
  en_US: 2026-11-20T6:56:43.091000 AMZ

レスポンスの日付を文字列化しているのは APIJSONEncoder のみで、生成側の
AutogenMethodHandlers.swift も全 150 箇所が self.after 経由のため、この1行で
手書き・生成の両経路が直る。

回帰テストとして、ネイティブが壊れた日付を返したことを Dart 側で検知する
テストを追加した (異常系の文字列は iOS 26.5 シミュレータでの実測値)。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W5pXxkWbYKDAnQZDpYu1RG
@masatoi
masatoi force-pushed the fix/ios-datetime-12hour-format branch from cce44cf to f3f9d1c Compare August 31, 2026 07:48
@masatoi masatoi changed the title fix: 12時間表示のiOS端末で日付が壊れ getTokenInfo が FormatException になる問題を修正 fix: date breaks on iOS devices set to 12-hour time, making getTokenInfo throw FormatException Aug 31, 2026

@ZweWaiYanHtet ZweWaiYanHtet left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@ZweWaiYanHtet
ZweWaiYanHtet merged commit da351bc into master Sep 1, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants