@namiml/expo-sdk 3.4.7-dev.202607230540 → 3.4.7-dev.202607231500

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@namiml/expo-sdk",
3
- "version": "3.4.7-dev.202607230540",
3
+ "version": "3.4.7-dev.202607231500",
4
4
  "type": "module",
5
5
  "description": "Nami Expo SDK — paywall and subscription management for Expo apps",
6
6
  "main": "./dist/index.cjs",
@@ -29,8 +29,8 @@
29
29
  "prepublishOnly": "yarn clean && yarn build:prod"
30
30
  },
31
31
  "dependencies": {
32
- "@namiml/expo-nami-iap": "3.4.7-dev.202607230540",
33
- "@namiml/sdk-core": "3.4.7-dev.202607230540",
32
+ "@namiml/expo-nami-iap": "3.4.7-dev.202607231500",
33
+ "@namiml/sdk-core": "3.4.7-dev.202607231500",
34
34
  "react-native-qrcode-svg": "^6.3.21",
35
35
  "react-native-safe-area-context": "^5.6.0",
36
36
  "react-native-svg": "^15.15.4"
@@ -1,4 +1,4 @@
1
- import React, { useCallback, useEffect, useState } from 'react';
1
+ import React, { useCallback, useEffect, useRef, useState } from 'react';
2
2
  import { TextInput, View, Text } from 'react-native';
3
3
  import type { ViewStyle } from 'react-native';
4
4
  import {
@@ -6,7 +6,9 @@ import {
6
6
  getDeviceFormFactor,
7
7
  validateTextInput,
8
8
  } from '@namiml/sdk-core';
9
- import { usePaywallContext } from '../../context/PaywallContext';
9
+ import { useFirstFocusReadyContext, usePaywallContext } from '../../context/PaywallContext';
10
+ import { useFocusEnabled, useRegisterPreferredFocus } from '../../context/FocusContext';
11
+ import { useTVPreferredFocus } from '../../utils/tvFocus';
10
12
  import {
11
13
  paddingAndMarginStyles,
12
14
  parseColor,
@@ -18,6 +20,7 @@ import {
18
20
  } from '../../utils/styles';
19
21
  import {
20
22
  TV_DEFAULTS,
23
+ resolveAutoFocus,
21
24
  resolveInputVisual,
22
25
  resolveLabelText,
23
26
  resolveSpacing,
@@ -45,11 +48,23 @@ interface Props {
45
48
  * Styling follows the 4-state precedence (error > focused > filled > empty) and TV
46
49
  * defaults, resolved in the pure `./textInputStyle` helpers. Form-factor detection
47
50
  * goes through core's `getDeviceFormFactor()` (honors `Nami.configure({ formFactor })`).
51
+ *
52
+ * `component.focused` (NAM-1529) drives autofocus on mount, mirroring the
53
+ * `NamiButton` `focused` precedent: `useTVPreferredFocus` (utils/tvFocus.ts) calls
54
+ * `.focus()` on mobile (raises the keyboard) but is a no-op on TV, where
55
+ * `hasTVPreferredFocus` sets d-pad focus only. The `focused` useState below is
56
+ * unrelated — it only drives the 4-state visual precedence.
48
57
  */
49
58
  export const NamiTextInput: React.FC<Props> = ({ component, scaleFactor, parentDirection }) => {
50
59
  const ctx = usePaywallContext();
60
+ const focusReadyCtx = useFirstFocusReadyContext();
61
+ const focusEnabled = useFocusEnabled();
51
62
  const formId = component.formId;
52
63
  const [focused, setFocused] = useState(false);
64
+ const inputRef = useRef<TextInput>(null);
65
+ const autoFocus = resolveAutoFocus(component, focusEnabled);
66
+ useTVPreferredFocus(inputRef, autoFocus);
67
+ useRegisterPreferredFocus(inputRef.current, autoFocus);
53
68
 
54
69
  // Seed form state + register the validator once per field.
55
70
  useEffect(() => {
@@ -76,6 +91,14 @@ export const NamiTextInput: React.FC<Props> = ({ component, scaleFactor, parentD
76
91
  }
77
92
  }, [formId, ctx, component]);
78
93
 
94
+ const handleFocus = useCallback(() => {
95
+ setFocused(true);
96
+ const paywallId = ctx.state.selectedPaywall?.id ?? 'unknown';
97
+ const page = ctx.state.currentPage ?? 'unknown';
98
+ const formFactor = ctx.state.formFactor;
99
+ focusReadyCtx.notifyFirstFocusReady(paywallId, page, formFactor);
100
+ }, [focusReadyCtx, ctx.state.selectedPaywall?.id, ctx.state.currentPage, ctx.state.formFactor]);
101
+
79
102
  const isTelevision = getDeviceFormFactor() === 'television';
80
103
  const visual = resolveInputVisual(component, { focused, hasError: !!error, isTelevision });
81
104
  const spacing = resolveSpacing(component, isTelevision);
@@ -136,9 +159,10 @@ export const NamiTextInput: React.FC<Props> = ({ component, scaleFactor, parentD
136
159
  <View style={wrapperStyle}>
137
160
  {showLabel ? <Text style={labelStyle}>{labelText}</Text> : null}
138
161
  <TextInput
162
+ ref={inputRef}
139
163
  value={value}
140
164
  onChangeText={onChangeText}
141
- onFocus={() => setFocused(true)}
165
+ onFocus={handleFocus}
142
166
  onBlur={() => setFocused(false)}
143
167
  placeholder={component.placeholderText}
144
168
  placeholderTextColor={parseColor(component.placeholderFontColor)}
@@ -146,6 +170,7 @@ export const NamiTextInput: React.FC<Props> = ({ component, scaleFactor, parentD
146
170
  autoCapitalize={isEmail ? 'none' : 'sentences'}
147
171
  autoCorrect={!isEmail}
148
172
  style={inputStyle}
173
+ hasTVPreferredFocus={autoFocus}
149
174
  accessibilityLabel={component.screenreaderText ?? labelText}
150
175
  accessibilityHint={component.screenreaderHint}
151
176
  />
@@ -124,3 +124,18 @@ export function resolveSpacing(component: TTextInputComponent, isTelevision: boo
124
124
  if (spacing == null) return isTelevision ? TV_DEFAULTS.spacing : 0;
125
125
  return Number(spacing);
126
126
  }
127
+
128
+ /**
129
+ * Whether this field should claim autofocus on mount (NAM-1529), mirroring the
130
+ * button `focused` precedent (`NamiButton.tsx`): the payload's `focused` flag
131
+ * only applies while the surrounding focus scope (`useFocusEnabled`) allows it,
132
+ * so an off-screen/inactive step never steals focus. The mobile-vs-TV split
133
+ * (keyboard raise vs. d-pad-only) lives in `useTVPreferredFocus`
134
+ * (utils/tvFocus.ts) — this only decides whether autofocus applies at all.
135
+ */
136
+ export function resolveAutoFocus(
137
+ component: Pick<TTextInputComponent, 'focused'>,
138
+ focusEnabled: boolean,
139
+ ): boolean {
140
+ return focusEnabled && Boolean(component.focused);
141
+ }