naeileun_pin_input 0.0.2 copy "naeileun_pin_input: ^0.0.2" to clipboard
naeileun_pin_input: ^0.0.2 copied to clipboard

6자리 PIN(간편 비밀번호) 입력용 모달 바텀시트 위젯. 입력 완료 후 외부 검증 → controller로 success/error/close 제어.

example/lib/main.dart

import 'package:flutter/material.dart';
import 'package:naeileun_pin_input/pin_input.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Pin Input Example',
      theme: ThemeData(primarySwatch: Colors.blue, useMaterial3: true),
      home: const HomePage(),
    );
  }
}

class HomePage extends StatefulWidget {
  const HomePage({super.key});

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  String? _enteredCode;
  String? _message;
  PinInputController? _controller;
  late final VoidCallback _controllerListener;

  bool _errorClearOnError = true;
  int _errorRevertMs = 800;
  String _errorMessage = '비밀번호가 올바르2지 않습니다.';
  String _validCode = '123456';

  @override
  void initState() {
    super.initState();
    _controllerListener = () {
      setState(() {
        _enteredCode = _controller?.text;
      });
    };
  }

  @override
  void dispose() {
    _controller?.removeListener(_controllerListener);
    _controller?.dispose();
    super.dispose();
  }

  void _setController(PinInputController controller) {
    _controller?.removeListener(_controllerListener);
    _controller?.dispose();
    _controller = controller;
    _controller?.addListener(_controllerListener);
    setState(() {
      _enteredCode = _controller?.text;
    });
  }

  void _openBottomSheet({
    required String title,
    required String description,
    required String shuffleText,
    PinInputTheme? theme,
    bool showSkipButton = false,
  }) {
    setState(() {
      _message = null;
      _enteredCode = null;
    });

    final controller = PinInputController();
    _setController(controller);

    SimpleVerificationBottomSheet.show(
      context: context,
      controller: controller,
      title: title,
      description: description,
      errorText: _errorMessage,
      shuffleText: shuffleText,
      theme: theme,
      showSkipButton: showSkipButton,
      titleStyle: const TextStyle(fontSize: 22, fontWeight: FontWeight.w700, color: Colors.black),
      descriptionStyle: TextStyle(
        fontSize: 15,
        fontWeight: FontWeight.w400,
        color: Colors.grey.shade700,
        height: 1.4,
      ),
      numberTextStyle: const TextStyle(
        fontSize: 24,
        fontWeight: FontWeight.w400,
        color: Colors.black,
      ),
      buttonTextStyle: TextStyle(
        fontSize: 15,
        fontWeight: FontWeight.w500,
        color: Colors.grey.shade800,
      ),
      errorTextStyle: TextStyle(
        fontSize: 14,
        fontWeight: FontWeight.w600,
        color: const Color(0xFFF44336),
      ),
      dotStyle: PinDotStyle(
        size: 14,
        spacing: 10,
        filledColor: (theme ?? PinInputTheme.defaultTheme()).domainColor,
        emptyColor: (theme ?? PinInputTheme.defaultTheme()).domainColor.withOpacity(0.2),
        errorFilledColor: const Color(0xFFF44336),
        errorEmptyColor: const Color(0xFFF44336).withOpacity(0.2),
      ),
      onInputComplete: (code) {
        setState(() {
          _message = '6자리 입력 완료: $code';
          _enteredCode = code;
        });
        // 외부에서 검증 -> controller로 success/error 처리
        if (code == _validCode) {
          controller.success();
          controller.close();
        } else {
          controller.error(
            clearOnError: _errorClearOnError,
            revertAfter: Duration(milliseconds: _errorRevertMs),
            message: _errorMessage,
          );
        }
      },
      onSkip: () {
        setState(() {
          _message = '건너뛰기를 선택했습니다.';
        });
      },
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Pin Input Example'),
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
      ),
      body: Center(
        child: Padding(
          padding: const EdgeInsets.all(24),
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              _buildStatusCard(),
              const SizedBox(height: 16),

              ElevatedButton(
                onPressed: () {
                  _openBottomSheet(
                    title: '간편 비밀번호를 입력해 주세요',
                    description: '계정 정보를 안전하게 지키고,\n다음부터는 간편하게 로그인할 수 있어요.',
                    shuffleText: '재배열',
                    showSkipButton: false,
                  );
                },
                style: ElevatedButton.styleFrom(
                  padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 16),
                ),
                child: const Text('바텀시트 열기 (기본)', style: TextStyle(fontSize: 16)),
              ),
              const SizedBox(height: 16),
              ElevatedButton(
                onPressed: () {
                  _openBottomSheet(
                    title: 'PIN 코드를 입력하세요',
                    description: '6자리 숫자를 입력해주세요.',
                    shuffleText: '섞기',
                    showSkipButton: true,
                  );
                },
                style: ElevatedButton.styleFrom(
                  padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 16),
                ),
                child: const Text('바텀시트 열기 (건너뛰기 포함)', style: TextStyle(fontSize: 16)),
              ),
              const SizedBox(height: 16),
              ElevatedButton(
                onPressed: () {
                  _openBottomSheet(
                    title: '커스텀 테마 테스트',
                    description: '테마 색상을 커스터마이징할 수 있습니다.',
                    shuffleText: '재배열',
                    showSkipButton: true,
                    theme: const PinInputTheme(
                      domainColor: Color(0xFF9C27B0),
                      emptyOpacity: 0.18,
                      descriptionOpacity: 0.70,
                      textOpacity: 0.90,
                      buttonTextOpacity: 0.90,
                    ),
                  );
                },
                style: ElevatedButton.styleFrom(
                  padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 16),
                ),
                child: const Text('커스텀 테마 테스트', style: TextStyle(fontSize: 16)),
              ),

              const SizedBox(height: 24),
              _buildControllerPanel(),
            ],
          ),
        ),
      ),
    );
  }

  Widget _buildStatusCard() {
    return Container(
      width: double.infinity,
      padding: const EdgeInsets.all(16),
      decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.circular(12),
        border: Border.all(color: Colors.grey.shade300),
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text('컨트롤러 상태', style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w700)),
          const SizedBox(height: 8),
          Text(
            'controller: ${_controller == null ? 'null' : 'active'}',
            style: TextStyle(fontSize: 14, color: Colors.grey.shade700),
          ),
          const SizedBox(height: 4),
          Text(
            'controller.text: ${(_controller?.text.isNotEmpty ?? false) ? _controller!.text : '(empty)'}',
            style: TextStyle(fontSize: 14, color: Colors.grey.shade700),
          ),
          if (_message != null) ...[
            const SizedBox(height: 8),
            Text(_message!, style: TextStyle(fontSize: 14, color: Colors.grey.shade800)),
          ],
          if (_enteredCode != null && _enteredCode!.isNotEmpty) ...[
            const SizedBox(height: 12),
            Text(
              '완료 코드: $_enteredCode',
              style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
            ),
          ],
        ],
      ),
    );
  }

  Widget _buildControllerPanel() {
    return Container(
      width: double.infinity,
      padding: const EdgeInsets.all(16),
      decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.circular(12),
        border: Border.all(color: Colors.grey.shade300),
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text('컨트롤러 조작', style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w700)),
          const SizedBox(height: 8),
          Row(
            children: [
              Expanded(
                child: OutlinedButton(
                  onPressed: _controller == null ? null : () => _controller!.success(),
                  child: const Text('success()', style: TextStyle(fontSize: 14)),
                ),
              ),
              const SizedBox(width: 12),
              Expanded(
                child: OutlinedButton(
                  onPressed: _controller == null
                      ? null
                      : () => _controller!.error(
                          clearOnError: _errorClearOnError,
                          revertAfter: Duration(milliseconds: _errorRevertMs),
                          message: _errorMessage,
                        ),
                  child: const Text('error()', style: TextStyle(fontSize: 14)),
                ),
              ),
            ],
          ),
          const SizedBox(height: 12),
          Text('검증용 정답 코드: $_validCode', style: const TextStyle(fontSize: 14)),
          const SizedBox(height: 6),
          TextField(
            decoration: const InputDecoration(
              border: OutlineInputBorder(),
              labelText: '에러 메시지 (errorText / controller.error(message))',
            ),
            onChanged: (v) => _errorMessage = v,
          ),
          const SizedBox(height: 12),
          Row(
            children: [
              Expanded(
                child: OutlinedButton(
                  onPressed: _controller == null ? null : () => _controller!.close(),
                  child: const Text('close()', style: TextStyle(fontSize: 14)),
                ),
              ),
              const SizedBox(width: 12),
              Expanded(
                child: OutlinedButton(
                  onPressed: _controller == null ? null : () => _controller!.clear(),
                  child: const Text('clear()', style: TextStyle(fontSize: 14)),
                ),
              ),
            ],
          ),
          const SizedBox(height: 12),
          Row(
            children: [
              Checkbox(
                value: _errorClearOnError,
                onChanged: (v) {
                  setState(() {
                    _errorClearOnError = v ?? true;
                  });
                },
              ),
              Expanded(
                child: const Text('error 시 입력 초기화(clearOnError)', style: TextStyle(fontSize: 14)),
              ),
            ],
          ),
          const SizedBox(height: 4),
          Row(
            children: [
              Text('revertAfter: $_errorRevertMs ms', style: const TextStyle(fontSize: 14)),
              Expanded(
                child: Slider(
                  value: _errorRevertMs.toDouble(),
                  min: 200,
                  max: 2000,
                  divisions: 18,
                  label: '$_errorRevertMs ms',
                  onChanged: (v) {
                    setState(() {
                      _errorRevertMs = v.round();
                    });
                  },
                ),
              ),
            ],
          ),
        ],
      ),
    );
  }
}
7
likes
125
points
141
downloads

Publisher

verified publishernaeileun.dev

Weekly Downloads

6자리 PIN(간편 비밀번호) 입력용 모달 바텀시트 위젯. 입력 완료 후 외부 검증 → controller로 success/error/close 제어.

Homepage

Topics

#flutter #ui #bottom-sheet #pin #security

Documentation

API reference

License

MIT (license)

Dependencies

flutter

More

Packages that depend on naeileun_pin_input