From adfe372d1327d6c0b1c1a0815aab04986aabbfc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torjus=20H=C3=A5kestad?= Date: Sun, 15 Feb 2026 09:34:56 +0100 Subject: [PATCH] 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 --- internal/shell/banking/model.go | 108 +++----------------- internal/shell/banking/screen_changepin.go | 111 +++++++++++++++++++++ 2 files changed, 125 insertions(+), 94 deletions(-) create mode 100644 internal/shell/banking/screen_changepin.go diff --git a/internal/shell/banking/model.go b/internal/shell/banking/model.go index f9599a2..090c099 100644 --- a/internal/shell/banking/model.go +++ b/internal/shell/banking/model.go @@ -42,10 +42,8 @@ type model struct { transfer transferModel history historyModel messages messagesModel - admin adminModel - pinInput string - pinStage int // 0=old, 1=new, 2=confirm, 3=done - pinMessage string + admin adminModel + changePin changePinModel } func newModel(sess *shell.SessionContext, bankName, terminalID, region string) *model { @@ -130,7 +128,7 @@ func (m *model) View() string { case screenMessages: content = m.messages.View() case screenChangePin: - content = m.viewChangePin() + content = m.changePin.View() case screenAdmin: 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")) case "6": m.screen = screenChangePin - m.pinInput = "" - m.pinStage = 0 - m.pinMessage = "" + m.changePin = newChangePinModel() return m, tea.Batch(tea.ClearScreen, logAction(m.sess, "MENU 6", "CHANGE PIN")) case "7": 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) { - keyMsg, ok := msg.(tea.KeyMsg) - if !ok { - return m, nil + prevStage := m.changePin.stage + var cmd tea.Cmd + 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 { - return m, m.goToMenu() + if m.changePin.done { + return m, tea.Batch(cmd, m.goToMenu()) } - - 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() + return m, cmd } func (m *model) updateAdmin(msg tea.Msg) (tea.Model, tea.Cmd) { diff --git a/internal/shell/banking/screen_changepin.go b/internal/shell/banking/screen_changepin.go new file mode 100644 index 0000000..118349d --- /dev/null +++ b/internal/shell/banking/screen_changepin.go @@ -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() +}