diff --git a/CHANGELOG.md b/CHANGELOG.md index ca845233..576531f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,22 @@ Full module documentation: [hexdocs.pm/mob](https://hexdocs.pm/mob). --- +## [Unreleased] + +### Added +- `text_field` now accepts a `secure: true` prop. iOS renders the field + as a SwiftUI `SecureField` (masked input) instead of the plain + `TextField`. The prop flows through the existing renderer + passthrough; cleartext still reaches the BEAM via `on_change` so apps + can hash/store the value as normal. Android consumes the same prop + via `PasswordVisualTransformation` once `mob_new`'s `MobBridge.kt.eex` + template is updated in a companion PR — until then the prop is a + graceful no-op on Android (renders as a regular field), no breakage. + + Reveal-toggle ("eye" button) is intentionally deferred — its + interaction with SwiftUI focus retention requires a `ZStack`-and-opacity + rebuild of `MobTextField` and warrants its own change. + ## [0.6.7] ### Added diff --git a/ios/MobNode.h b/ios/MobNode.h index 8f8ba9a4..a21cae7e 100644 --- a/ios/MobNode.h +++ b/ios/MobNode.h @@ -125,6 +125,7 @@ NS_ASSUME_NONNULL_BEGIN @property(nonatomic, copy, nonnull) NSString *keyboardTypeStr; // "default","number","decimal","email","phone","url" @property(nonatomic, copy, nonnull) NSString *returnKeyStr; // "done","next","go","search","send" +@property(nonatomic, assign) BOOL isSecure; // mask input (SecureField on iOS) @property(nonatomic, copy, nullable) void (^onFocus)(void); @property(nonatomic, copy, nullable) void (^onBlur)(void); @property(nonatomic, copy, nullable) void (^onSubmit)(void); diff --git a/ios/MobRootView.swift b/ios/MobRootView.swift index 8701aba6..6a9ae1fb 100644 --- a/ios/MobRootView.swift +++ b/ios/MobRootView.swift @@ -1028,8 +1028,17 @@ private struct MobTextField: View { } } + @ViewBuilder + private var field: some View { + if node.isSecure { + SecureField(placeholder, text: $text) + } else { + TextField(placeholder, text: $text) + } + } + var body: some View { - TextField(placeholder, text: $text) + field .focused($isFocused) .keyboardType(keyboardType) .submitLabel(submitLabel) diff --git a/ios/mob_nif.m b/ios/mob_nif.m index 0fc43bc3..2a91a112 100644 --- a/ios/mob_nif.m +++ b/ios/mob_nif.m @@ -797,6 +797,10 @@ static void mob_send_change_float(int handle, double value) { if ([returnKey isKindOfClass:[NSString class]]) node.returnKeyStr = returnKey; + id secure = props[@"secure"]; + if ([secure isKindOfClass:[NSNumber class]]) + node.isSecure = [secure boolValue]; + id onFocus = props[@"on_focus"]; if (onFocus && [onFocus isKindOfClass:[NSNumber class]]) { int handle = [onFocus intValue]; diff --git a/test/mob/renderer_test.exs b/test/mob/renderer_test.exs index 1f35da2c..ad924e82 100644 --- a/test/mob/renderer_test.exs +++ b/test/mob/renderer_test.exs @@ -544,6 +544,22 @@ defmodule Mob.RendererTest do assert decoded["props"]["return_key"] == "next" end + test "secure boolean is passed through unchanged" do + tree = %{type: :text_field, props: %{value: "", secure: true}, children: []} + Renderer.render(tree, :android, MockNIF) + {:set_root, [json]} = Enum.find(MockNIF.calls(), fn {f, _} -> f == :set_root end) + decoded = :json.decode(json) + assert decoded["props"]["secure"] == true + end + + test "secure defaults to absent when unset" do + tree = %{type: :text_field, props: %{value: ""}, children: []} + Renderer.render(tree, :android, MockNIF) + {:set_root, [json]} = Enum.find(MockNIF.calls(), fn {f, _} -> f == :set_root end) + decoded = :json.decode(json) + refute Map.has_key?(decoded["props"], "secure") + end + test "register_tap receives {pid, tag} for tagged taps" do pid = self() tree = %{type: :button, props: %{text: "Tap", on_tap: {pid, :my_action}}, children: []}