@salve-software/react-native-nitro-input-mask 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (204) hide show
  1. package/LICENSE +21 -0
  2. package/NitroInputMask.podspec +31 -0
  3. package/README.md +181 -0
  4. package/android/CMakeLists.txt +32 -0
  5. package/android/build.gradle +148 -0
  6. package/android/fix-prefab.gradle +51 -0
  7. package/android/gradle.properties +5 -0
  8. package/android/src/main/AndroidManifest.xml +2 -0
  9. package/android/src/main/cpp/cpp-adapter.cpp +9 -0
  10. package/android/src/main/java/com/nitroinputmask/NitroInputMaskContext.kt +7 -0
  11. package/android/src/main/java/com/nitroinputmask/NitroInputMaskModule.kt +108 -0
  12. package/android/src/main/java/com/nitroinputmask/NitroInputMaskPackage.kt +29 -0
  13. package/android/src/main/java/com/nitroinputmask/NitroInputMaskServiceModule.kt +17 -0
  14. package/android/src/main/java/com/nitroinputmask/NitroInputMaskTextWatcher.kt +127 -0
  15. package/android/src/main/java/com/nitroinputmask/engine/CursorEngine.kt +30 -0
  16. package/android/src/main/java/com/nitroinputmask/engine/MaskEngine.kt +261 -0
  17. package/android/src/main/java/com/nitroinputmask/engine/MaskEngineFactory.kt +18 -0
  18. package/android/src/main/java/com/nitroinputmask/engine/MaskEngineProtocol.kt +8 -0
  19. package/android/src/main/java/com/nitroinputmask/masks/CreditCardMaskEngine.kt +47 -0
  20. package/android/src/main/java/com/nitroinputmask/masks/CustomMaskEngine.kt +16 -0
  21. package/android/src/main/java/com/nitroinputmask/masks/DatetimeMaskEngine.kt +38 -0
  22. package/android/src/main/java/com/nitroinputmask/masks/MoneyMaskEngine.kt +66 -0
  23. package/ios/Bridge.h +8 -0
  24. package/ios/NitroInputMaskDelegateProxy.swift +194 -0
  25. package/ios/NitroInputMaskModule.swift +114 -0
  26. package/ios/NitroInputMaskServiceModule.swift +11 -0
  27. package/ios/engine/CursorEngine.swift +52 -0
  28. package/ios/engine/MaskEngine.swift +221 -0
  29. package/ios/engine/MaskEngineFactory.swift +16 -0
  30. package/ios/engine/MaskEngineProtocol.swift +7 -0
  31. package/ios/masks/CreditCardMaskEngine.swift +57 -0
  32. package/ios/masks/CustomMaskEngine.swift +16 -0
  33. package/ios/masks/DatetimeMaskEngine.swift +46 -0
  34. package/ios/masks/MoneyMaskEngine.swift +66 -0
  35. package/lib/commonjs/classes/NitroInputMaskService/NitroInputMaskService.class.js +74 -0
  36. package/lib/commonjs/classes/NitroInputMaskService/NitroInputMaskService.class.js.map +1 -0
  37. package/lib/commonjs/classes/NitroInputMaskService/index.js +13 -0
  38. package/lib/commonjs/classes/NitroInputMaskService/index.js.map +1 -0
  39. package/lib/commonjs/classes/NitroInputMaskService/types/IApplyMaskProps.js +6 -0
  40. package/lib/commonjs/classes/NitroInputMaskService/types/IApplyMaskProps.js.map +1 -0
  41. package/lib/commonjs/classes/NitroInputMaskService/types/index.js +2 -0
  42. package/lib/commonjs/classes/NitroInputMaskService/types/index.js.map +1 -0
  43. package/lib/commonjs/classes/index.js +17 -0
  44. package/lib/commonjs/classes/index.js.map +1 -0
  45. package/lib/commonjs/components/NitroInputMask/index.js +41 -0
  46. package/lib/commonjs/components/NitroInputMask/index.js.map +1 -0
  47. package/lib/commonjs/components/NitroInputMask/types/NitroInputMaskProps.js +6 -0
  48. package/lib/commonjs/components/NitroInputMask/types/NitroInputMaskProps.js.map +1 -0
  49. package/lib/commonjs/components/NitroInputMask/types/index.js +2 -0
  50. package/lib/commonjs/components/NitroInputMask/types/index.js.map +1 -0
  51. package/lib/commonjs/components/index.js +17 -0
  52. package/lib/commonjs/components/index.js.map +1 -0
  53. package/lib/commonjs/index.js +39 -0
  54. package/lib/commonjs/index.js.map +1 -0
  55. package/lib/commonjs/nitro-module.js +17 -0
  56. package/lib/commonjs/nitro-module.js.map +1 -0
  57. package/lib/commonjs/package.json +1 -0
  58. package/lib/commonjs/specs/nitro-input-mask-service.nitro.js +6 -0
  59. package/lib/commonjs/specs/nitro-input-mask-service.nitro.js.map +1 -0
  60. package/lib/commonjs/specs/nitro-input-mask.nitro.js +6 -0
  61. package/lib/commonjs/specs/nitro-input-mask.nitro.js.map +1 -0
  62. package/lib/commonjs/types/CreditCardMaskOptions.js +2 -0
  63. package/lib/commonjs/types/CreditCardMaskOptions.js.map +1 -0
  64. package/lib/commonjs/types/CustomMaskOptions.js +2 -0
  65. package/lib/commonjs/types/CustomMaskOptions.js.map +1 -0
  66. package/lib/commonjs/types/DatetimeMaskOptions.js +2 -0
  67. package/lib/commonjs/types/DatetimeMaskOptions.js.map +1 -0
  68. package/lib/commonjs/types/MaskConfig.js +6 -0
  69. package/lib/commonjs/types/MaskConfig.js.map +1 -0
  70. package/lib/commonjs/types/MoneyMaskOptions.js +2 -0
  71. package/lib/commonjs/types/MoneyMaskOptions.js.map +1 -0
  72. package/lib/commonjs/types/index.js +2 -0
  73. package/lib/commonjs/types/index.js.map +1 -0
  74. package/lib/module/classes/NitroInputMaskService/NitroInputMaskService.class.js +70 -0
  75. package/lib/module/classes/NitroInputMaskService/NitroInputMaskService.class.js.map +1 -0
  76. package/lib/module/classes/NitroInputMaskService/index.js +4 -0
  77. package/lib/module/classes/NitroInputMaskService/index.js.map +1 -0
  78. package/lib/module/classes/NitroInputMaskService/types/IApplyMaskProps.js +4 -0
  79. package/lib/module/classes/NitroInputMaskService/types/IApplyMaskProps.js.map +1 -0
  80. package/lib/module/classes/NitroInputMaskService/types/index.js +2 -0
  81. package/lib/module/classes/NitroInputMaskService/types/index.js.map +1 -0
  82. package/lib/module/classes/index.js +4 -0
  83. package/lib/module/classes/index.js.map +1 -0
  84. package/lib/module/components/NitroInputMask/index.js +35 -0
  85. package/lib/module/components/NitroInputMask/index.js.map +1 -0
  86. package/lib/module/components/NitroInputMask/types/NitroInputMaskProps.js +4 -0
  87. package/lib/module/components/NitroInputMask/types/NitroInputMaskProps.js.map +1 -0
  88. package/lib/module/components/NitroInputMask/types/index.js +2 -0
  89. package/lib/module/components/NitroInputMask/types/index.js.map +1 -0
  90. package/lib/module/components/index.js +4 -0
  91. package/lib/module/components/index.js.map +1 -0
  92. package/lib/module/index.js +6 -0
  93. package/lib/module/index.js.map +1 -0
  94. package/lib/module/nitro-module.js +12 -0
  95. package/lib/module/nitro-module.js.map +1 -0
  96. package/lib/module/specs/nitro-input-mask-service.nitro.js +4 -0
  97. package/lib/module/specs/nitro-input-mask-service.nitro.js.map +1 -0
  98. package/lib/module/specs/nitro-input-mask.nitro.js +4 -0
  99. package/lib/module/specs/nitro-input-mask.nitro.js.map +1 -0
  100. package/lib/module/types/CreditCardMaskOptions.js +2 -0
  101. package/lib/module/types/CreditCardMaskOptions.js.map +1 -0
  102. package/lib/module/types/CustomMaskOptions.js +2 -0
  103. package/lib/module/types/CustomMaskOptions.js.map +1 -0
  104. package/lib/module/types/DatetimeMaskOptions.js +2 -0
  105. package/lib/module/types/DatetimeMaskOptions.js.map +1 -0
  106. package/lib/module/types/MaskConfig.js +4 -0
  107. package/lib/module/types/MaskConfig.js.map +1 -0
  108. package/lib/module/types/MoneyMaskOptions.js +2 -0
  109. package/lib/module/types/MoneyMaskOptions.js.map +1 -0
  110. package/lib/module/types/index.js +2 -0
  111. package/lib/module/types/index.js.map +1 -0
  112. package/lib/typescript/src/classes/NitroInputMaskService/NitroInputMaskService.class.d.ts +50 -0
  113. package/lib/typescript/src/classes/NitroInputMaskService/NitroInputMaskService.class.d.ts.map +1 -0
  114. package/lib/typescript/src/classes/NitroInputMaskService/index.d.ts +3 -0
  115. package/lib/typescript/src/classes/NitroInputMaskService/index.d.ts.map +1 -0
  116. package/lib/typescript/src/classes/NitroInputMaskService/types/IApplyMaskProps.d.ts +5 -0
  117. package/lib/typescript/src/classes/NitroInputMaskService/types/IApplyMaskProps.d.ts.map +1 -0
  118. package/lib/typescript/src/classes/NitroInputMaskService/types/index.d.ts +2 -0
  119. package/lib/typescript/src/classes/NitroInputMaskService/types/index.d.ts.map +1 -0
  120. package/lib/typescript/src/classes/index.d.ts +2 -0
  121. package/lib/typescript/src/classes/index.d.ts.map +1 -0
  122. package/lib/typescript/src/components/NitroInputMask/index.d.ts +5 -0
  123. package/lib/typescript/src/components/NitroInputMask/index.d.ts.map +1 -0
  124. package/lib/typescript/src/components/NitroInputMask/types/NitroInputMaskProps.d.ts +4 -0
  125. package/lib/typescript/src/components/NitroInputMask/types/NitroInputMaskProps.d.ts.map +1 -0
  126. package/lib/typescript/src/components/NitroInputMask/types/index.d.ts +2 -0
  127. package/lib/typescript/src/components/NitroInputMask/types/index.d.ts.map +1 -0
  128. package/lib/typescript/src/components/index.d.ts +2 -0
  129. package/lib/typescript/src/components/index.d.ts.map +1 -0
  130. package/lib/typescript/src/index.d.ts +4 -0
  131. package/lib/typescript/src/index.d.ts.map +1 -0
  132. package/lib/typescript/src/nitro-module.d.ts +5 -0
  133. package/lib/typescript/src/nitro-module.d.ts.map +1 -0
  134. package/lib/typescript/src/specs/nitro-input-mask-service.nitro.d.ts +9 -0
  135. package/lib/typescript/src/specs/nitro-input-mask-service.nitro.d.ts.map +1 -0
  136. package/lib/typescript/src/specs/nitro-input-mask.nitro.d.ts +23 -0
  137. package/lib/typescript/src/specs/nitro-input-mask.nitro.d.ts.map +1 -0
  138. package/lib/typescript/src/types/CreditCardMaskOptions.d.ts +5 -0
  139. package/lib/typescript/src/types/CreditCardMaskOptions.d.ts.map +1 -0
  140. package/lib/typescript/src/types/CustomMaskOptions.d.ts +4 -0
  141. package/lib/typescript/src/types/CustomMaskOptions.d.ts.map +1 -0
  142. package/lib/typescript/src/types/DatetimeMaskOptions.d.ts +4 -0
  143. package/lib/typescript/src/types/DatetimeMaskOptions.d.ts.map +1 -0
  144. package/lib/typescript/src/types/MaskConfig.d.ts +18 -0
  145. package/lib/typescript/src/types/MaskConfig.d.ts.map +1 -0
  146. package/lib/typescript/src/types/MoneyMaskOptions.d.ts +9 -0
  147. package/lib/typescript/src/types/MoneyMaskOptions.d.ts.map +1 -0
  148. package/lib/typescript/src/types/index.d.ts +6 -0
  149. package/lib/typescript/src/types/index.d.ts.map +1 -0
  150. package/nitro.json +40 -0
  151. package/nitrogen/generated/.gitattributes +1 -0
  152. package/nitrogen/generated/android/NitroInputMask+autolinking.cmake +83 -0
  153. package/nitrogen/generated/android/NitroInputMask+autolinking.gradle +27 -0
  154. package/nitrogen/generated/android/NitroInputMaskOnLoad.cpp +70 -0
  155. package/nitrogen/generated/android/NitroInputMaskOnLoad.hpp +34 -0
  156. package/nitrogen/generated/android/c++/JHybridNitroInputMaskServiceSpec.cpp +57 -0
  157. package/nitrogen/generated/android/c++/JHybridNitroInputMaskServiceSpec.hpp +63 -0
  158. package/nitrogen/generated/android/c++/JHybridNitroInputMaskSpec.cpp +68 -0
  159. package/nitrogen/generated/android/c++/JHybridNitroInputMaskSpec.hpp +66 -0
  160. package/nitrogen/generated/android/c++/JNitroMaskOptions.hpp +94 -0
  161. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroinputmask/HybridNitroInputMaskServiceSpec.kt +54 -0
  162. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroinputmask/HybridNitroInputMaskSpec.kt +66 -0
  163. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroinputmask/NitroInputMaskOnLoad.kt +35 -0
  164. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroinputmask/NitroMaskOptions.kt +96 -0
  165. package/nitrogen/generated/ios/NitroInputMask+autolinking.rb +62 -0
  166. package/nitrogen/generated/ios/NitroInputMask-Swift-Cxx-Bridge.cpp +50 -0
  167. package/nitrogen/generated/ios/NitroInputMask-Swift-Cxx-Bridge.hpp +124 -0
  168. package/nitrogen/generated/ios/NitroInputMask-Swift-Cxx-Umbrella.hpp +53 -0
  169. package/nitrogen/generated/ios/NitroInputMaskAutolinking.mm +41 -0
  170. package/nitrogen/generated/ios/NitroInputMaskAutolinking.swift +38 -0
  171. package/nitrogen/generated/ios/c++/HybridNitroInputMaskServiceSpecSwift.cpp +11 -0
  172. package/nitrogen/generated/ios/c++/HybridNitroInputMaskServiceSpecSwift.hpp +85 -0
  173. package/nitrogen/generated/ios/c++/HybridNitroInputMaskSpecSwift.cpp +11 -0
  174. package/nitrogen/generated/ios/c++/HybridNitroInputMaskSpecSwift.hpp +101 -0
  175. package/nitrogen/generated/ios/swift/HybridNitroInputMaskServiceSpec.swift +55 -0
  176. package/nitrogen/generated/ios/swift/HybridNitroInputMaskServiceSpec_cxx.swift +138 -0
  177. package/nitrogen/generated/ios/swift/HybridNitroInputMaskSpec.swift +58 -0
  178. package/nitrogen/generated/ios/swift/HybridNitroInputMaskSpec_cxx.swift +170 -0
  179. package/nitrogen/generated/ios/swift/NitroMaskOptions.swift +204 -0
  180. package/nitrogen/generated/shared/c++/HybridNitroInputMaskServiceSpec.cpp +21 -0
  181. package/nitrogen/generated/shared/c++/HybridNitroInputMaskServiceSpec.hpp +64 -0
  182. package/nitrogen/generated/shared/c++/HybridNitroInputMaskSpec.cpp +24 -0
  183. package/nitrogen/generated/shared/c++/HybridNitroInputMaskSpec.hpp +67 -0
  184. package/nitrogen/generated/shared/c++/NitroMaskOptions.hpp +120 -0
  185. package/package.json +136 -0
  186. package/src/classes/NitroInputMaskService/NitroInputMaskService.class.ts +70 -0
  187. package/src/classes/NitroInputMaskService/index.ts +2 -0
  188. package/src/classes/NitroInputMaskService/types/IApplyMaskProps.ts +3 -0
  189. package/src/classes/NitroInputMaskService/types/index.ts +1 -0
  190. package/src/classes/index.ts +1 -0
  191. package/src/components/NitroInputMask/index.tsx +41 -0
  192. package/src/components/NitroInputMask/types/NitroInputMaskProps.ts +4 -0
  193. package/src/components/NitroInputMask/types/index.ts +1 -0
  194. package/src/components/index.ts +1 -0
  195. package/src/index.ts +3 -0
  196. package/src/nitro-module.ts +14 -0
  197. package/src/specs/nitro-input-mask-service.nitro.ts +6 -0
  198. package/src/specs/nitro-input-mask.nitro.ts +25 -0
  199. package/src/types/CreditCardMaskOptions.ts +4 -0
  200. package/src/types/CustomMaskOptions.ts +3 -0
  201. package/src/types/DatetimeMaskOptions.ts +3 -0
  202. package/src/types/MaskConfig.ts +10 -0
  203. package/src/types/MoneyMaskOptions.ts +8 -0
  204. package/src/types/index.ts +5 -0
@@ -0,0 +1,194 @@
1
+ import UIKit
2
+
3
+ class NitroInputMaskDelegateProxy: NSObject, UITextFieldDelegate {
4
+ var engine: MaskEngineProtocol
5
+ weak var original: UITextFieldDelegate?
6
+
7
+ init(engine: MaskEngineProtocol, original: UITextFieldDelegate?) {
8
+ self.engine = engine
9
+ self.original = original
10
+ }
11
+
12
+ func textField(
13
+ _ textField: UITextField,
14
+ shouldChangeCharactersIn range: NSRange,
15
+ replacementString string: String
16
+ ) -> Bool {
17
+ let current = textField.text ?? ""
18
+ guard let swiftRange = Range(range, in: current) else { return false }
19
+ let proposed = current.replacingCharacters(in: swiftRange, with: string)
20
+
21
+ let isDeletion = string.isEmpty
22
+ var (masked, raw) = engine.apply(input: proposed)
23
+
24
+ // Empty mask (custom with no mask string) — pass through
25
+ if masked.isEmpty && raw.isEmpty && !proposed.isEmpty {
26
+ let (_, currentRaw) = engine.apply(input: current)
27
+ if currentRaw.isEmpty {
28
+ textField.text = proposed
29
+ textField.sendActions(for: .editingChanged)
30
+ return false
31
+ }
32
+ }
33
+
34
+ // Separator deletion: masking re-produces the same text because the separator
35
+ // is always re-inserted. If there is data, block the deletion (text unchanged,
36
+ // cursor moves to just before the separator). If there is no data, clear the field.
37
+ var isSeparatorDeletion = false
38
+ if isDeletion && masked == current {
39
+ let (_, extractedRaw) = engine.apply(input: current)
40
+ if extractedRaw.isEmpty {
41
+ masked = ""
42
+ } else {
43
+ isSeparatorDeletion = true
44
+ // masked stays == current; cursor will move to range.location below
45
+ }
46
+ }
47
+
48
+ textField.text = masked
49
+
50
+ if engine.wantsTrailingCursor {
51
+ let cursorPos: Int
52
+ if isSeparatorDeletion {
53
+ // Separator deleted: move to just before it so next backspace hits a real digit.
54
+ cursorPos = range.location
55
+ } else if isDeletion {
56
+ // If the cursor was at the trailing edge (no digits after the deleted
57
+ // char), stay at the end of the new string — RTL money digits shift left
58
+ // and the "digits before" anchor would land one position short.
59
+ // Otherwise use the digits-before anchor so the cursor survives a
60
+ // thousands separator being removed (e.g. 1.123,55 → 112,55).
61
+ let digitsAfterDeleted = countDigits(in: current, from: range.location + range.length)
62
+
63
+ if digitsAfterDeleted == 0 {
64
+ cursorPos = masked.count
65
+ } else {
66
+ let digitsBeforeRange = countDigitsUpTo(in: current, upTo: range.location)
67
+ cursorPos = positionAfterDigits(digitsBeforeRange, in: masked)
68
+ }
69
+ } else {
70
+ // Insertion: use "digits from end" so cursor survives magnitude changes.
71
+ let digitsAfterCursor = countDigits(in: current, from: range.location + range.length)
72
+ cursorPos = cursorPositionWithDigitsAfter(digitsAfterCursor, in: masked)
73
+ }
74
+ if let pos = textField.position(from: textField.beginningOfDocument, offset: cursorPos) {
75
+ textField.selectedTextRange = textField.textRange(from: pos, to: pos)
76
+ }
77
+ } else if let expandedMask = engine.expandedMask {
78
+ let cursorOffset: Int
79
+
80
+ if isDeletion {
81
+ cursorOffset = min(range.location, masked.count)
82
+ } else {
83
+ let raw = CursorEngine.offsetAfterInsertion(
84
+ oldMasked: current,
85
+ newMasked: masked,
86
+ mask: expandedMask,
87
+ at: range.location
88
+ )
89
+ // Advance past any auto-inserted literals immediately following the cursor.
90
+ cursorOffset = skipLeadingLiterals(at: raw, in: masked, mask: expandedMask)
91
+ }
92
+ if let pos = textField.position(from: textField.beginningOfDocument, offset: cursorOffset) {
93
+ textField.selectedTextRange = textField.textRange(from: pos, to: pos)
94
+ }
95
+ } else {
96
+ let endPos = masked.count
97
+
98
+ if let pos = textField.position(from: textField.beginningOfDocument, offset: endPos) {
99
+ textField.selectedTextRange = textField.textRange(from: pos, to: pos)
100
+ }
101
+ }
102
+
103
+ textField.sendActions(for: .editingChanged)
104
+ return false
105
+ }
106
+
107
+ func textFieldShouldReturn(_ textField: UITextField) -> Bool {
108
+ textField.resignFirstResponder()
109
+ return original?.textFieldShouldReturn?(textField) ?? true
110
+ }
111
+
112
+ override func responds(to aSelector: Selector!) -> Bool {
113
+ super.responds(to: aSelector) || (original?.responds(to: aSelector) ?? false)
114
+ }
115
+
116
+ override func forwardingTarget(for aSelector: Selector!) -> Any? {
117
+ if original?.responds(to: aSelector) == true { return original }
118
+ return super.forwardingTarget(for: aSelector)
119
+ }
120
+
121
+ // MARK: - Cursor helpers
122
+
123
+ /// Advances `offset` past any consecutive literal slots in `mask`, so the
124
+ /// cursor lands after auto-inserted separators rather than before them.
125
+ private func skipLeadingLiterals(at offset: Int, in masked: String, mask: String) -> Int {
126
+ let dataTokens: Set<Character> = ["9", "A", "*"]
127
+ let maskChars = Array(mask)
128
+ var idx = offset
129
+
130
+ while idx < maskChars.count && !dataTokens.contains(maskChars[idx]) {
131
+ idx += 1
132
+ }
133
+ return min(idx, masked.count)
134
+ }
135
+
136
+ /// Counts digit characters in `text` before index `upTo`.
137
+ private func countDigitsUpTo(in text: String, upTo: Int) -> Int {
138
+ let chars = Array(text)
139
+ let limit = min(upTo, chars.count)
140
+ return chars[0..<limit].filter { $0.isNumber }.count
141
+ }
142
+
143
+ /// Returns the position in `text` immediately after the `count`-th digit.
144
+ /// When `count` is 0, returns the index of the first digit (past any unit
145
+ /// prefix) so the cursor lands before the content, not before the unit.
146
+ /// Falls back to `text.count` when fewer digits exist.
147
+ private func positionAfterDigits(_ count: Int, in text: String) -> Int {
148
+ let chars = Array(text)
149
+
150
+ if count == 0 {
151
+ return chars.enumerated().first(where: { $0.element.isNumber })?.offset ?? 0
152
+ }
153
+
154
+ var found = 0
155
+ for (i, c) in chars.enumerated() {
156
+ if c.isNumber {
157
+ found += 1
158
+ if found == count { return i + 1 }
159
+ }
160
+ }
161
+ return text.count
162
+ }
163
+
164
+ /// Returns how many digit characters exist in `text` starting from `start`.
165
+ private func countDigits(in text: String, from start: Int) -> Int {
166
+ guard start < text.count else { return 0 }
167
+ let chars = Array(text)
168
+ var count = 0
169
+
170
+ for i in start..<chars.count where chars[i].isNumber {
171
+ count += 1
172
+ }
173
+ return count
174
+ }
175
+
176
+ /// Returns the cursor position in `text` such that exactly `count` digit
177
+ /// characters appear after it. Used for RTL money masks so the cursor
178
+ /// stays near the edit point after reformatting.
179
+ private func cursorPositionWithDigitsAfter(_ count: Int, in text: String) -> Int {
180
+ if count == 0 { return text.count }
181
+ let chars = Array(text)
182
+ var digitsFromEnd = 0
183
+ var pos = chars.count
184
+
185
+ while pos > 0 {
186
+ pos -= 1
187
+ if chars[pos].isNumber {
188
+ digitsFromEnd += 1
189
+ if digitsFromEnd == count { return pos }
190
+ }
191
+ }
192
+ return 0
193
+ }
194
+ }
@@ -0,0 +1,114 @@
1
+ import NitroModules
2
+ import UIKit
3
+
4
+ private let attachRetries = 5
5
+ private let attachRetryDelay: TimeInterval = 0.05
6
+
7
+ class HybridNitroInputMaskModule: HybridNitroInputMaskSpec_base, HybridNitroInputMaskSpec_protocol {
8
+ private var proxies: [String: NitroInputMaskDelegateProxy] = [:]
9
+
10
+ func attach(nativeID: String, maskType: String, options: NitroMaskOptions) throws {
11
+ let engine = MaskEngineFactory.build(maskType: maskType, options: options)
12
+ attemptAttach(nativeID: nativeID, engine: engine, retries: attachRetries)
13
+ }
14
+
15
+ private func attemptAttach(nativeID: String, engine: MaskEngineProtocol, retries: Int) {
16
+ DispatchQueue.main.async { [weak self] in
17
+ guard let self else { return }
18
+
19
+ if let tf = self.findTextField(nativeID: nativeID) {
20
+ let proxy = NitroInputMaskDelegateProxy(engine: engine, original: tf.delegate)
21
+ tf.delegate = proxy
22
+ self.proxies[nativeID] = proxy
23
+ } else if retries > 0 {
24
+ DispatchQueue.main.asyncAfter(deadline: .now() + attachRetryDelay) { [weak self] in
25
+ self?.attemptAttach(nativeID: nativeID, engine: engine, retries: retries - 1)
26
+ }
27
+ }
28
+ }
29
+ }
30
+
31
+ func detach(nativeID: String) throws {
32
+ DispatchQueue.main.async { [weak self] in
33
+ guard let self else { return }
34
+ guard let proxy = self.proxies.removeValue(forKey: nativeID) else { return }
35
+
36
+ if let tf = self.findTextField(nativeID: nativeID) {
37
+ tf.delegate = proxy.original
38
+ }
39
+ }
40
+ }
41
+
42
+ func updateMask(nativeID: String, maskType: String, options: NitroMaskOptions) throws {
43
+ let engine = MaskEngineFactory.build(maskType: maskType, options: options)
44
+ proxies[nativeID]?.engine = engine
45
+ }
46
+
47
+ func setValue(nativeID: String, rawValue: String) throws {
48
+ DispatchQueue.main.async { [weak self] in
49
+ guard let self else { return }
50
+ guard let proxy = self.proxies[nativeID] else { return }
51
+ guard let tf = self.findTextField(nativeID: nativeID) else { return }
52
+
53
+ let (masked, _) = proxy.engine.apply(input: rawValue)
54
+ guard tf.text != masked else { return }
55
+ tf.text = masked
56
+
57
+ if proxy.engine.wantsTrailingCursor {
58
+ let endPos = masked.count
59
+ if let pos = tf.position(from: tf.beginningOfDocument, offset: endPos) {
60
+ tf.selectedTextRange = tf.textRange(from: pos, to: pos)
61
+ }
62
+ } else if let expandedMask = proxy.engine.expandedMask {
63
+ CursorEngine.apply(to: tf, masked: masked, mask: expandedMask)
64
+ }
65
+ }
66
+ }
67
+
68
+ private func findTextField(nativeID: String) -> UITextField? {
69
+ let windows: [UIWindow]
70
+
71
+ if #available(iOS 15.0, *) {
72
+ windows = UIApplication.shared.connectedScenes
73
+ .compactMap { $0 as? UIWindowScene }
74
+ .flatMap { $0.windows }
75
+ } else {
76
+ windows = UIApplication.shared.windows
77
+ }
78
+
79
+ for window in windows {
80
+ if let container = findView(in: window, identifier: nativeID),
81
+ let tf = findFirstTextField(in: container)
82
+ {
83
+ return tf
84
+ }
85
+ }
86
+ return nil
87
+ }
88
+
89
+ private func findView(in view: UIView, identifier: String) -> UIView? {
90
+ for selName in ["nativeId", "nativeID"] {
91
+ let sel = NSSelectorFromString(selName)
92
+ if view.responds(to: sel),
93
+ let id = view.perform(sel)?.takeUnretainedValue() as? String,
94
+ id == identifier
95
+ {
96
+ return view
97
+ }
98
+ }
99
+
100
+ for sv in view.subviews {
101
+ if let found = findView(in: sv, identifier: identifier) { return found }
102
+ }
103
+ return nil
104
+ }
105
+
106
+ private func findFirstTextField(in view: UIView) -> UITextField? {
107
+ if let tf = view as? UITextField { return tf }
108
+
109
+ for sv in view.subviews {
110
+ if let found = findFirstTextField(in: sv) { return found }
111
+ }
112
+ return nil
113
+ }
114
+ }
@@ -0,0 +1,11 @@
1
+ import NitroModules
2
+
3
+ class HybridNitroInputMaskServiceModule: HybridNitroInputMaskServiceSpec_base,
4
+ HybridNitroInputMaskServiceSpec_protocol
5
+ {
6
+ func applyMask(value: String, maskType: String, options: NitroMaskOptions) throws -> String {
7
+ let engine = MaskEngineFactory.build(maskType: maskType, options: options)
8
+ let (masked, _) = engine.apply(input: value)
9
+ return masked
10
+ }
11
+ }
@@ -0,0 +1,52 @@
1
+ import UIKit
2
+
3
+ enum CursorEngine {
4
+ static func offsetAtEnd(masked: String, mask: String) -> Int {
5
+ let maskChars = Array(mask)
6
+ var result = masked.count
7
+ for i in stride(from: min(masked.count, mask.count) - 1, through: 0, by: -1) {
8
+ let m = maskChars[i]
9
+ if m == "9" || m == "A" || m == "*" {
10
+ result = i + 1
11
+ break
12
+ }
13
+ }
14
+ return result
15
+ }
16
+
17
+ /// Returns the cursor offset after inserting one character at `insertionPoint`
18
+ /// in `oldMasked`, producing `newMasked`.
19
+ static func offsetAfterInsertion(
20
+ oldMasked: String, newMasked: String, mask: String, at insertionPoint: Int
21
+ ) -> Int {
22
+ let maskChars = Array(mask)
23
+
24
+ // Count data chars before insertionPoint in the old masked string
25
+ var dataRankBefore = 0
26
+ for i in 0..<min(insertionPoint, maskChars.count) {
27
+ let m = maskChars[i]
28
+ if m == "9" || m == "A" || m == "*" { dataRankBefore += 1 }
29
+ }
30
+
31
+ // Find position after the (dataRankBefore + 1)-th data char in new masked string
32
+ let targetRank = dataRankBefore + 1
33
+ var dataCount = 0
34
+ for i in 0..<min(newMasked.count, maskChars.count) {
35
+ let m = maskChars[i]
36
+ if m == "9" || m == "A" || m == "*" {
37
+ dataCount += 1
38
+ if dataCount == targetRank { return i + 1 }
39
+ }
40
+ }
41
+
42
+ return newMasked.count
43
+ }
44
+
45
+ static func apply(to textField: UITextField, masked: String, mask: String) {
46
+ let off = offsetAtEnd(masked: masked, mask: mask)
47
+ guard let pos = textField.position(from: textField.beginningOfDocument, offset: off) else {
48
+ return
49
+ }
50
+ textField.selectedTextRange = textField.textRange(from: pos, to: pos)
51
+ }
52
+ }
@@ -0,0 +1,221 @@
1
+ import Foundation
2
+
3
+ // MARK: - Slot
4
+
5
+ private enum Slot {
6
+ case digit
7
+ case letter
8
+ case any
9
+ case literal(Character)
10
+ case range(blockName: String, length: Int, from: Int, to: Int)
11
+ }
12
+
13
+ // MARK: - CompiledMask
14
+
15
+ struct CompiledMask {
16
+ fileprivate let slots: [Slot]
17
+ let expandedMask: String
18
+ var isEmpty: Bool { slots.isEmpty }
19
+ }
20
+
21
+ // MARK: - MaskEngine
22
+
23
+ enum MaskEngine {
24
+
25
+ // MARK: compile
26
+
27
+ /// Compiles a mask string into a `CompiledMask`.
28
+ ///
29
+ /// Supported tokens:
30
+ /// - `9` — any digit
31
+ /// - `A` — any letter
32
+ /// - `*` — any alphanumeric
33
+ /// - `[from-to]` — integer range, e.g. `[1-12]`, `[1-31]`, `[0-23]`
34
+ /// - any other character — literal
35
+ ///
36
+ /// Note: two `[from-to]` blocks with identical bounds share the same
37
+ /// `blockName` key and therefore share partial-tracking state during
38
+ /// `apply`. This is an acceptable trade-off; prefer distinct bounds when
39
+ /// designing date masks (e.g. `[1-12]/[1-31]/9999`).
40
+ static func compile(mask: String) -> CompiledMask {
41
+ var slots: [Slot] = []
42
+ var expandedMask = ""
43
+ var i = mask.startIndex
44
+
45
+ while i < mask.endIndex {
46
+ if mask[i] == "[" {
47
+ guard let closeIdx = mask[i...].firstIndex(of: "]") else {
48
+ // Malformed — treat "[" as a literal and advance past it.
49
+ slots.append(.literal("["))
50
+ expandedMask.append("[")
51
+ i = mask.index(after: i)
52
+ continue
53
+ }
54
+
55
+ let content = String(mask[mask.index(after: i)..<closeIdx]) // e.g. "1-12"
56
+ let parts = content.split(separator: "-", maxSplits: 1)
57
+ if parts.count == 2, let from = Int(parts[0]), let to = Int(parts[1]) {
58
+ let length = max(String(from).count, String(to).count)
59
+ for _ in 0..<length {
60
+ slots.append(.range(blockName: content, length: length, from: from, to: to))
61
+ expandedMask.append("9")
62
+ }
63
+ } else {
64
+ // Malformed range — emit the entire "[...]" as individual literals.
65
+ let literal = "[" + content + "]"
66
+ for c in literal {
67
+ slots.append(.literal(c))
68
+ expandedMask.append(c)
69
+ }
70
+ }
71
+ i = mask.index(after: closeIdx)
72
+ } else {
73
+ switch mask[i] {
74
+ case "9":
75
+ slots.append(.digit)
76
+ expandedMask.append("9")
77
+
78
+ case "A":
79
+ slots.append(.letter)
80
+ expandedMask.append("A")
81
+
82
+ case "*":
83
+ slots.append(.any)
84
+ expandedMask.append("*")
85
+
86
+ default:
87
+ slots.append(.literal(mask[i]))
88
+ expandedMask.append(mask[i])
89
+ }
90
+ i = mask.index(after: i)
91
+ }
92
+ }
93
+
94
+ return CompiledMask(slots: slots, expandedMask: expandedMask)
95
+ }
96
+
97
+ // MARK: apply
98
+
99
+ static func apply(input: String, compiled: CompiledMask) -> (masked: String, raw: String) {
100
+ if compiled.slots.isEmpty { return (input, input) }
101
+
102
+ var masked = ""
103
+ var raw = ""
104
+ let slots = compiled.slots
105
+ let inputChars = Array(input)
106
+ var ii = 0
107
+ var si = 0
108
+ var blockPartials: [String: String] = [:]
109
+
110
+ while si < slots.count && ii < inputChars.count {
111
+ let slot = slots[si]
112
+ let c = inputChars[ii]
113
+
114
+ switch slot {
115
+ case .digit:
116
+ if c.isNumber {
117
+ masked.append(c)
118
+ raw.append(c)
119
+ ii += 1
120
+ si += 1
121
+ } else {
122
+ ii += 1
123
+ }
124
+
125
+ case .letter:
126
+ if c.isLetter {
127
+ masked.append(c)
128
+ raw.append(c)
129
+ ii += 1
130
+ si += 1
131
+ } else {
132
+ ii += 1
133
+ }
134
+
135
+ case .any:
136
+ if c.isLetter || c.isNumber {
137
+ masked.append(c)
138
+ raw.append(c)
139
+ ii += 1
140
+ si += 1
141
+ } else {
142
+ ii += 1
143
+ }
144
+
145
+ case .literal(let lc):
146
+ masked.append(lc)
147
+ if c == lc { ii += 1 }
148
+ si += 1
149
+
150
+ case .range(let blockName, let length, let from, let to):
151
+ if c.isNumber {
152
+ let partial = blockPartials[blockName, default: ""]
153
+ let newPartial = partial + String(c)
154
+ if isValidPrefix(partial: newPartial, from: from, to: to, length: length) {
155
+ masked.append(c)
156
+ raw.append(c)
157
+ blockPartials[blockName] = newPartial
158
+ ii += 1
159
+ si += 1
160
+ } else {
161
+ ii += 1
162
+ }
163
+ } else {
164
+ ii += 1
165
+ }
166
+ }
167
+ }
168
+
169
+ // Only emit trailing literals if some input was actually consumed.
170
+ if ii > 0 {
171
+ while si < slots.count, case .literal(let lc) = slots[si] {
172
+ masked.append(lc)
173
+ si += 1
174
+ }
175
+ }
176
+
177
+ return (masked, raw)
178
+ }
179
+
180
+ // MARK: extractRaw
181
+
182
+ static func extractRaw(from text: String, compiled: CompiledMask) -> String {
183
+ return extractRaw(from: text, mask: compiled.expandedMask)
184
+ }
185
+
186
+ // MARK: Private helpers
187
+
188
+ private static func extractRaw(from text: String, mask: String) -> String {
189
+ var raw = ""
190
+ let textChars = Array(text)
191
+ let maskChars = Array(mask)
192
+ var ti = 0
193
+ var mi = 0
194
+
195
+ while ti < textChars.count && mi < maskChars.count {
196
+ let m = maskChars[mi]
197
+
198
+ if m == "9" || m == "A" || m == "*" {
199
+ raw.append(textChars[ti])
200
+ ti += 1
201
+ mi += 1
202
+ } else {
203
+ if textChars[ti] == maskChars[mi] { ti += 1 }
204
+ mi += 1
205
+ }
206
+ }
207
+
208
+ return raw
209
+ }
210
+
211
+ private static func isValidPrefix(partial: String, from: Int, to: Int, length: Int) -> Bool {
212
+ let remaining = length - partial.count
213
+ guard remaining >= 0 else { return false }
214
+ guard
215
+ let partialMin = Int(partial + String(repeating: "0", count: remaining)),
216
+ let partialMax = Int(partial + String(repeating: "9", count: remaining))
217
+ else { return false }
218
+
219
+ return partialMin <= to && partialMax >= from
220
+ }
221
+ }
@@ -0,0 +1,16 @@
1
+ import NitroModules
2
+
3
+ enum MaskEngineFactory {
4
+ static func build(maskType: String, options: NitroMaskOptions) -> MaskEngineProtocol {
5
+ switch maskType {
6
+ case "money":
7
+ return MoneyMaskEngine(options: options)
8
+ case "datetime":
9
+ return DatetimeMaskEngine(format: options.format ?? "DD/MM/YYYY")
10
+ case "credit-card":
11
+ return CreditCardMaskEngine(options: options)
12
+ default: // "custom"
13
+ return CustomMaskEngine(mask: options.mask ?? "")
14
+ }
15
+ }
16
+ }
@@ -0,0 +1,7 @@
1
+ protocol MaskEngineProtocol {
2
+ func apply(input: String) -> (masked: String, raw: String)
3
+ var wantsTrailingCursor: Bool { get }
4
+ /// Expanded mask string used for cursor positioning (e.g. "999.999.999-99").
5
+ /// Return nil if cursor should just go to end of masked string.
6
+ var expandedMask: String? { get }
7
+ }
@@ -0,0 +1,57 @@
1
+ import NitroModules
2
+
3
+ struct CreditCardMaskEngine: MaskEngineProtocol {
4
+ private let compiled: CompiledMask
5
+ private let obfuscated: Bool
6
+
7
+ init(options: NitroMaskOptions) {
8
+ let issuer = options.issuer ?? "visa-or-mastercard"
9
+ self.obfuscated = options.obfuscated ?? false
10
+
11
+ let mask: String
12
+ switch issuer {
13
+ case "amex":
14
+ mask = "9999 999999 99999"
15
+ case "diners":
16
+ mask = "9999 999999 9999"
17
+ default: // visa-or-mastercard
18
+ mask = "9999 9999 9999 9999"
19
+ }
20
+
21
+ self.compiled = MaskEngine.compile(mask: mask)
22
+ }
23
+
24
+ func apply(input: String) -> (masked: String, raw: String) {
25
+ let (masked, raw) = MaskEngine.apply(input: input, compiled: compiled)
26
+
27
+ if obfuscated && !masked.isEmpty {
28
+ let obfuscatedMasked = CreditCardMaskEngine.obfuscate(masked)
29
+ return (obfuscatedMasked, raw)
30
+ }
31
+
32
+ return (masked, raw)
33
+ }
34
+
35
+ var wantsTrailingCursor: Bool { false }
36
+ var expandedMask: String? { compiled.expandedMask }
37
+
38
+ // MARK: - Obfuscation
39
+
40
+ private static func obfuscate(_ masked: String) -> String {
41
+ guard let lastSpaceIdx = masked.lastIndex(of: " ") else {
42
+ // No spaces — single group, don't obfuscate
43
+ return masked
44
+ }
45
+
46
+ let lastGroupStart = masked.index(after: lastSpaceIdx)
47
+ let prefix = String(masked[masked.startIndex..<lastGroupStart])
48
+ let lastGroup = String(masked[lastGroupStart...])
49
+
50
+ // Replace digit characters with * in prefix, keep spaces intact
51
+ let obfuscatedPrefix = prefix.map { c -> Character in
52
+ c.isNumber ? "*" : c
53
+ }
54
+
55
+ return String(obfuscatedPrefix) + lastGroup
56
+ }
57
+ }
@@ -0,0 +1,16 @@
1
+ import NitroModules
2
+
3
+ struct CustomMaskEngine: MaskEngineProtocol {
4
+ private let compiled: CompiledMask
5
+
6
+ init(mask: String) {
7
+ self.compiled = MaskEngine.compile(mask: mask)
8
+ }
9
+
10
+ func apply(input: String) -> (masked: String, raw: String) {
11
+ return MaskEngine.apply(input: input, compiled: compiled)
12
+ }
13
+
14
+ var wantsTrailingCursor: Bool { false }
15
+ var expandedMask: String? { compiled.expandedMask }
16
+ }