refactor: extract changePinModel into its own sub-model

The Change PIN screen was the only screen with its state (pinInput,
pinStage, pinMessage) stored directly on the top-level model. Extract
it into a changePinModel in screen_changepin.go to match the pattern
used by all other screens.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-15 09:34:56 +01:00
parent 3163ea47dc
commit adfe372d13
2 changed files with 125 additions and 94 deletions

View File

@@ -42,10 +42,8 @@ type model struct {
transfer transferModel transfer transferModel
history historyModel history historyModel
messages messagesModel messages messagesModel
admin adminModel admin adminModel
pinInput string changePin changePinModel
pinStage int // 0=old, 1=new, 2=confirm, 3=done
pinMessage string
} }
func newModel(sess *shell.SessionContext, bankName, terminalID, region string) *model { func newModel(sess *shell.SessionContext, bankName, terminalID, region string) *model {
@@ -130,7 +128,7 @@ func (m *model) View() string {
case screenMessages: case screenMessages:
content = m.messages.View() content = m.messages.View()
case screenChangePin: case screenChangePin:
content = m.viewChangePin() content = m.changePin.View()
case screenAdmin: case screenAdmin:
content = m.admin.View() content = m.admin.View()
} }
@@ -182,9 +180,7 @@ func (m *model) updateMenu(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, tea.Batch(tea.ClearScreen, logAction(m.sess, "MENU 5", "SECURE MESSAGES")) return m, tea.Batch(tea.ClearScreen, logAction(m.sess, "MENU 5", "SECURE MESSAGES"))
case "6": case "6":
m.screen = screenChangePin m.screen = screenChangePin
m.pinInput = "" m.changePin = newChangePinModel()
m.pinStage = 0
m.pinMessage = ""
return m, tea.Batch(tea.ClearScreen, logAction(m.sess, "MENU 6", "CHANGE PIN")) return m, tea.Batch(tea.ClearScreen, logAction(m.sess, "MENU 6", "CHANGE PIN"))
case "7": case "7":
m.quitting = true m.quitting = true
@@ -278,95 +274,19 @@ func (m *model) updateMessages(msg tea.Msg) (tea.Model, tea.Cmd) {
} }
func (m *model) updateChangePin(msg tea.Msg) (tea.Model, tea.Cmd) { func (m *model) updateChangePin(msg tea.Msg) (tea.Model, tea.Cmd) {
keyMsg, ok := msg.(tea.KeyMsg) prevStage := m.changePin.stage
if !ok { var cmd tea.Cmd
return m, nil m.changePin, cmd = m.changePin.Update(msg)
// Log successful PIN change.
if m.changePin.stage == 3 && prevStage != 3 {
cmd = tea.Batch(cmd, logAction(m.sess, "CHANGE PIN", "PIN CHANGED SUCCESSFULLY"))
} }
if m.pinStage == 3 { if m.changePin.done {
return m, m.goToMenu() return m, tea.Batch(cmd, m.goToMenu())
} }
return m, cmd
switch keyMsg.Type {
case tea.KeyEnter:
switch m.pinStage {
case 0:
if m.pinInput != "" {
m.pinStage = 1
m.pinInput = ""
}
case 1:
if len(m.pinInput) >= 4 {
m.pinMessage = m.pinInput
m.pinStage = 2
m.pinInput = ""
}
case 2:
if m.pinInput == m.pinMessage {
m.pinStage = 3
return m, logAction(m.sess, "CHANGE PIN", "PIN CHANGED SUCCESSFULLY")
}
m.pinInput = ""
m.pinMessage = ""
m.pinStage = 1
}
case tea.KeyEscape:
return m, m.goToMenu()
case tea.KeyBackspace:
if len(m.pinInput) > 0 {
m.pinInput = m.pinInput[:len(m.pinInput)-1]
}
default:
ch := keyMsg.String()
if len(ch) == 1 && ch[0] >= 32 && ch[0] < 127 && len(m.pinInput) < 12 {
m.pinInput += ch
}
}
return m, nil
}
func (m *model) viewChangePin() string {
var b strings.Builder
b.WriteString("\n")
b.WriteString(centerText("CHANGE PIN"))
b.WriteString("\n\n")
b.WriteString(thinDivider())
b.WriteString("\n\n")
if m.pinStage == 3 {
b.WriteString(titleStyle.Render(" PIN CHANGED SUCCESSFULLY"))
b.WriteString("\n\n")
b.WriteString(baseStyle.Render(" YOUR NEW PIN IS NOW ACTIVE."))
b.WriteString("\n")
b.WriteString(baseStyle.Render(" PLEASE USE YOUR NEW PIN FOR ALL FUTURE TRANSACTIONS."))
b.WriteString("\n\n")
b.WriteString(dimStyle.Render(" PRESS ANY KEY TO RETURN TO MAIN MENU"))
} else {
prompts := []string{" CURRENT PIN: ", " NEW PIN: ", " CONFIRM PIN: "}
for i := 0; i < m.pinStage; i++ {
b.WriteString(baseStyle.Render(prompts[i]))
b.WriteString(baseStyle.Render(strings.Repeat("*", 4)))
b.WriteString("\n")
}
if m.pinStage < 3 {
b.WriteString(titleStyle.Render(prompts[m.pinStage]))
masked := strings.Repeat("*", len(m.pinInput))
b.WriteString(inputStyle.Render(masked))
b.WriteString(inputStyle.Render("_"))
b.WriteString("\n")
}
b.WriteString("\n")
if m.pinStage == 1 {
b.WriteString(dimStyle.Render(" PIN MUST BE AT LEAST 4 CHARACTERS"))
b.WriteString("\n")
}
b.WriteString("\n")
b.WriteString(dimStyle.Render(" PRESS ESC TO RETURN TO MAIN MENU"))
}
b.WriteString("\n")
return b.String()
} }
func (m *model) updateAdmin(msg tea.Msg) (tea.Model, tea.Cmd) { func (m *model) updateAdmin(msg tea.Msg) (tea.Model, tea.Cmd) {

View File

@@ -0,0 +1,111 @@
package banking
import (
"strings"
tea "github.com/charmbracelet/bubbletea"
)
type changePinModel struct {
input string
stage int // 0=old, 1=new, 2=confirm, 3=done
newPin string
done bool
}
func newChangePinModel() changePinModel {
return changePinModel{}
}
func (m changePinModel) Update(msg tea.Msg) (changePinModel, tea.Cmd) {
keyMsg, ok := msg.(tea.KeyMsg)
if !ok {
return m, nil
}
if m.stage == 3 {
m.done = true
return m, nil
}
switch keyMsg.Type {
case tea.KeyEnter:
switch m.stage {
case 0:
if m.input != "" {
m.stage = 1
m.input = ""
}
case 1:
if len(m.input) >= 4 {
m.newPin = m.input
m.stage = 2
m.input = ""
}
case 2:
if m.input == m.newPin {
m.stage = 3
} else {
m.input = ""
m.newPin = ""
m.stage = 1
}
}
case tea.KeyEscape:
m.done = true
case tea.KeyBackspace:
if len(m.input) > 0 {
m.input = m.input[:len(m.input)-1]
}
default:
ch := keyMsg.String()
if len(ch) == 1 && ch[0] >= 32 && ch[0] < 127 && len(m.input) < 12 {
m.input += ch
}
}
return m, nil
}
func (m changePinModel) View() string {
var b strings.Builder
b.WriteString("\n")
b.WriteString(centerText("CHANGE PIN"))
b.WriteString("\n\n")
b.WriteString(thinDivider())
b.WriteString("\n\n")
if m.stage == 3 {
b.WriteString(titleStyle.Render(" PIN CHANGED SUCCESSFULLY"))
b.WriteString("\n\n")
b.WriteString(baseStyle.Render(" YOUR NEW PIN IS NOW ACTIVE."))
b.WriteString("\n")
b.WriteString(baseStyle.Render(" PLEASE USE YOUR NEW PIN FOR ALL FUTURE TRANSACTIONS."))
b.WriteString("\n\n")
b.WriteString(dimStyle.Render(" PRESS ANY KEY TO RETURN TO MAIN MENU"))
} else {
prompts := []string{" CURRENT PIN: ", " NEW PIN: ", " CONFIRM PIN: "}
for i := 0; i < m.stage; i++ {
b.WriteString(baseStyle.Render(prompts[i]))
b.WriteString(baseStyle.Render(strings.Repeat("*", 4)))
b.WriteString("\n")
}
if m.stage < 3 {
b.WriteString(titleStyle.Render(prompts[m.stage]))
masked := strings.Repeat("*", len(m.input))
b.WriteString(inputStyle.Render(masked))
b.WriteString(inputStyle.Render("_"))
b.WriteString("\n")
}
b.WriteString("\n")
if m.stage == 1 {
b.WriteString(dimStyle.Render(" PIN MUST BE AT LEAST 4 CHARACTERS"))
b.WriteString("\n")
}
b.WriteString("\n")
b.WriteString(dimStyle.Render(" PRESS ESC TO RETURN TO MAIN MENU"))
}
b.WriteString("\n")
return b.String()
}