<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: tuan fan</title>
    <description>The latest articles on DEV Community by tuan fan (@tuan_fan_b37a2d370946f5db).</description>
    <link>https://dev.to/tuan_fan_b37a2d370946f5db</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4074188%2Fa45bff3a-a96a-4646-9824-394b827c1c08.png</url>
      <title>DEV Community: tuan fan</title>
      <link>https://dev.to/tuan_fan_b37a2d370946f5db</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tuan_fan_b37a2d370946f5db"/>
    <language>en</language>
    <item>
      <title>LarkSIM giffgaff 英国实体卡，购卡前先确认充值方式</title>
      <dc:creator>tuan fan</dc:creator>
      <pubDate>Tue, 08 Sep 2026 11:38:51 +0000</pubDate>
      <link>https://dev.to/tuan_fan_b37a2d370946f5db/larksim-giffgaff-ying-guo-shi-ti-qia-gou-qia-qian-xian-que-ren-chong-zhi-fang-shi-4j14</link>
      <guid>https://dev.to/tuan_fan_b37a2d370946f5db/larksim-giffgaff-ying-guo-shi-ti-qia-gou-qia-qian-xian-que-ren-chong-zhi-fang-shi-4j14</guid>
      <description>&lt;p&gt;准备买一张英国实体 SIM 卡时，先确认自己有没有可用的充值方式。下面这份说明由 LarkSIM 提供，内容按当前产品页整理，价格核对时间为 2026 年 9 月 8 日。&lt;/p&gt;

&lt;p&gt;&lt;a href="https://larksim.com/sim/giffgaff?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=directory_listing&amp;amp;utm_content=giffgaff_sim" rel="noopener noreferrer"&gt;LarkSIM giffgaff 英国实体 SIM 卡&lt;/a&gt;带有真实英国手机号，用户收到卡后自行注册、激活并管理 giffgaff 账户。LarkSIM 提供实体卡寄送，以及中文激活、保号和漫游使用指导。&lt;/p&gt;

&lt;p&gt;如果已经有可用的付款方式，或愿意自己准备充值券，可以选择 &lt;a href="https://larksim.com/checkout?product=blank" rel="noopener noreferrer"&gt;giffgaff 自助卡&lt;/a&gt;。当前价格为每张 15 美元，附中文教程，不含充值券。买卡以后，首次充值仍需要自己完成。&lt;/p&gt;

&lt;p&gt;&lt;a href="https://larksim.com/checkout?product=activated" rel="noopener noreferrer"&gt;giffgaff 省心卡&lt;/a&gt;当前为每张 43 美元，内含一张 £10 官方充值券，并提供中文激活与使用咨询。首次充值可以兑换这张券，无需另行准备海外银行卡。账户注册、充值券兑换和激活仍由用户本人操作。&lt;/p&gt;

&lt;p&gt;已经有 giffgaff 卡或账户，也可以单独购买 &lt;a href="https://larksim.com/checkout?product=giffgaff-topup-10" rel="noopener noreferrer"&gt;£10 官方充值券&lt;/a&gt;。当前售价为 23 美元，交付数字卡密，不包含实体卡。&lt;/p&gt;

&lt;p&gt;如果购卡目的是接收 Codex、X 等海外平台的登录验证码，还要先核对目标平台是否接受该号码。验证码能否送达，取决于目标平台规则与网络条件，购买任何版本都不能保证结果。下单前可阅读产品页附带的中文指南，再根据自己的充值条件选择。&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Designing SMS Login Flows for Roaming Users</title>
      <dc:creator>tuan fan</dc:creator>
      <pubDate>Wed, 12 Aug 2026 07:54:05 +0000</pubDate>
      <link>https://dev.to/tuan_fan_b37a2d370946f5db/designing-sms-login-flows-for-roaming-users-1okp</link>
      <guid>https://dev.to/tuan_fan_b37a2d370946f5db/designing-sms-login-flows-for-roaming-users-1okp</guid>
      <description>&lt;p&gt;An SMS login flow looks simple: generate a code, send it, and verify the response. Roaming turns that operation into a chain of independent systems.&lt;/p&gt;

&lt;p&gt;Your application may accept the request. Your messaging provider may accept the message. A carrier may route it toward the subscriber's home network. A visited network may register the handset. The phone may be connected but using the wrong SIM for messages. The destination service may also reject a number range or pause an account before any message is sent.&lt;/p&gt;

&lt;p&gt;That is why “the code did not arrive” is not a diagnosis. It is only a symptom.&lt;/p&gt;

&lt;p&gt;The goal is to make every stage observable, keep retries safe, and give users a useful next action when the route is uncertain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Model delivery as a state machine
&lt;/h2&gt;

&lt;p&gt;Do not store one boolean called &lt;code&gt;smsSent&lt;/code&gt;. “Sent” often means only that your provider accepted an API request.&lt;/p&gt;

&lt;p&gt;A more useful model separates application, provider, delivery, and verification state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;LoginCodeState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;created&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;provider_accepted&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;provider_rejected&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;delivery_reported&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;delivery_unknown&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;expired&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;verified&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;rate_limited&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;FailureClass&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;invalid_number&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;provider_configuration&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;carrier_or_route&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;account_policy&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user_input&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;unknown&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;LoginCodeAttempt&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;accountId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;phoneRef&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;countryCallingCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;providerMessageId&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;LoginCodeState&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;failureClass&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;FailureClass&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;createdAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;expiresAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;verifiedAt&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Make &lt;code&gt;phoneRef&lt;/code&gt; a controlled token or an HMAC produced with a server-held key, not a plain hash of an enumerable phone-number space.&lt;/p&gt;

&lt;p&gt;The request path should create an attempt before calling the provider, attach an idempotency key, and update the attempt with the provider's response. A later delivery receipt can move it again, but lack of a receipt must remain &lt;code&gt;delivery_unknown&lt;/code&gt;, not silently become “delivered.”&lt;/p&gt;

&lt;p&gt;Also remember that delivery receipts are not uniformly available or equally authoritative across routes. Your UI should not claim more certainty than your telemetry provides.&lt;/p&gt;

&lt;h2&gt;
  
  
  Normalize the number once
&lt;/h2&gt;

&lt;p&gt;Store and send phone numbers in E.164 format. Keep country selection separate from the national number in the UI, then normalize on the server.&lt;/p&gt;

&lt;p&gt;For example, a UK mobile number written domestically with a leading &lt;code&gt;0&lt;/code&gt; becomes &lt;code&gt;+44&lt;/code&gt; followed by the remaining digits. If the user selects &lt;code&gt;+44&lt;/code&gt;, do not let them submit another &lt;code&gt;44&lt;/code&gt; inside the national-number field.&lt;/p&gt;

&lt;p&gt;Validate syntax, numbering-plan plausibility, and current service policy separately. Policy, provider coverage, fraud controls, and routing can change even when the number remains valid.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build a user-facing diagnostic path
&lt;/h2&gt;

&lt;p&gt;Use this runbook in order.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Confirm whether a request was created
&lt;/h3&gt;

&lt;p&gt;Return a correlation ID to the client and show a neutral state such as “Request received.” If validation, rate limiting, or the provider call failed, record that explicitly.&lt;/p&gt;

&lt;p&gt;Keep the public response generic enough to avoid account enumeration. Detailed failure data belongs in authenticated support tooling, not in a message that reveals whether a phone number has an account.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Check normalized input
&lt;/h3&gt;

&lt;p&gt;Display a masked destination and country code before sending. Let the user correct the number instead of repeatedly requesting codes to the wrong destination.&lt;/p&gt;

&lt;p&gt;Common errors include a duplicated country code, a retained domestic trunk prefix, whitespace copied from a contact, or the number for the wrong SIM on a dual-SIM phone.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Read the provider result accurately
&lt;/h3&gt;

&lt;p&gt;Separate immediate rejection from provider acceptance. Capture the provider message ID, timestamps in UTC, error category, and any delivery status that arrives later.&lt;/p&gt;

&lt;p&gt;An immediate rejection usually points to input, credentials, sender configuration, unsupported destinations, or provider policy. An accepted request with no delivery confirmation requires a different investigation; it may be a carrier route, filtering, roaming registration, or simply missing receipt data.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Test an ordinary SMS
&lt;/h3&gt;

&lt;p&gt;Ask the user to receive one normal text from a trusted contact. This is the most useful branch in the whole decision tree.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If ordinary SMS also fails, investigate the SIM, account state, handset, signal, and roaming registration first.&lt;/li&gt;
&lt;li&gt;If ordinary SMS works but every application-to-person code fails, investigate the messaging route, sender identity, and carrier filtering.&lt;/li&gt;
&lt;li&gt;If ordinary SMS works and only one service fails, the likely boundary is that service's number support, sending route, cooldown, or account policy.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ordinary SMS success proves that part of the mobile path works. It does not guarantee that a particular verification sender will use the same path or accept the same number range.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Check the roaming environment
&lt;/h3&gt;

&lt;p&gt;For a roaming user, collect a small, non-sensitive checklist:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is the relevant SIM line enabled?&lt;/li&gt;
&lt;li&gt;Is the handset registered on a visited network?&lt;/li&gt;
&lt;li&gt;Is automatic network selection enabled?&lt;/li&gt;
&lt;li&gt;On a dual-SIM device, is the expected line active for incoming messages?&lt;/li&gt;
&lt;li&gt;Did a restart or flight-mode toggle refresh registration?&lt;/li&gt;
&lt;li&gt;Can the user receive messages after moving to an area with stronger signal?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Mobile data and SMS are not the same test. A correctly registered line may receive SMS while mobile data is disabled.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Stop the resend storm
&lt;/h3&gt;

&lt;p&gt;Repeated clicks create ambiguity. They can trigger rate limits, invalidate older codes depending on the implementation, cause messages to arrive out of order, and make the user try the wrong value.&lt;/p&gt;

&lt;p&gt;Allow one active code per account and destination. Apply idempotency for accidental double clicks, show a visible cooldown, and invalidate previous codes when a new one is issued. After a bounded number of attempts, stop sending and route the user to support or another authentication factor.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. Escalate with evidence, not secrets
&lt;/h3&gt;

&lt;p&gt;A useful escalation includes the correlation ID, UTC timestamps, masked number, country code, provider message ID, state transitions, device type, and the result of the ordinary-SMS test.&lt;/p&gt;

&lt;p&gt;Never include the code itself, full phone number, passwords, recovery codes, or message content in routine logs. Use access controls and short retention for the remaining diagnostic data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Design the fallback before the failure
&lt;/h2&gt;

&lt;p&gt;SMS should not be the only recovery path. Offer passkeys, authenticator apps, or recovery codes; use verified email only for lower-assurance recovery where policy permits.&lt;/p&gt;

&lt;p&gt;Fallback does not mean weakening the same control after repeated failures. Support agents should not forward codes, disclose account state, or manually override risk checks because a user says they are abroad. A legitimate fallback should be designed, audited, and protected before the incident occurs.&lt;/p&gt;

&lt;p&gt;Set honest UI expectations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;“Request accepted” is better than “Code sent” when delivery is unknown.&lt;/li&gt;
&lt;li&gt;Show the code lifetime and resend cooldown.&lt;/li&gt;
&lt;li&gt;Explain the expected international number format.&lt;/li&gt;
&lt;li&gt;Provide a short diagnostic link before the user opens a ticket.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Test the failure matrix
&lt;/h2&gt;

&lt;p&gt;Maintain test-owned numbers and cover at least these cases:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Expected system behavior&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Invalid international format&lt;/td&gt;
&lt;td&gt;Reject before provider call&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Duplicate click&lt;/td&gt;
&lt;td&gt;Reuse the idempotent attempt&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Provider rejects request&lt;/td&gt;
&lt;td&gt;Record a classified failure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Provider accepts, no receipt&lt;/td&gt;
&lt;td&gt;Keep delivery as unknown&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Code arrives after expiry&lt;/td&gt;
&lt;td&gt;Reject it; record it under the verification-attempt policy without consuming a new send&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;User requests a new code&lt;/td&gt;
&lt;td&gt;Invalidate the previous code&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Roaming SIM receives ordinary SMS but not OTP&lt;/td&gt;
&lt;td&gt;Escalate sender or route evidence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Attempt limit reached&lt;/td&gt;
&lt;td&gt;Stop sends and offer a legitimate fallback&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Run the matrix across the countries and carriers you actually support. Do not infer global compatibility from one successful handset.&lt;/p&gt;

&lt;h2&gt;
  
  
  Disclosure and operating context
&lt;/h2&gt;

&lt;p&gt;Disclosure: I help operate LarkSim, an independent seller and guide for physical giffgaff SIM cards. The ordinary-SMS-first branch in this article grew out of support work documented in this &lt;a href="https://larksim.com/en/guide/giffgaff-sms-verification" rel="noopener noreferrer"&gt;roaming SMS verification troubleshooting guide&lt;/a&gt;. That operational experience is not a claim that giffgaff, or any other carrier or number range, will receive codes from every platform.&lt;/p&gt;

&lt;p&gt;The broader lesson is provider-independent: authentication teams own the application state and telemetry, carriers own parts of the route, and destination platforms own their acceptance and risk rules. A trustworthy login flow makes those boundaries visible.&lt;/p&gt;

&lt;p&gt;Design for uncertainty. Record what happened. Give users one safe next step. Never turn a successful test into a delivery guarantee.&lt;/p&gt;

</description>
      <category>authentication</category>
      <category>security</category>
      <category>webdev</category>
      <category>sms</category>
    </item>
  </channel>
</rss>
