irismail 0.1.0 → 0.5.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.
package/README.md CHANGED
@@ -1,157 +1,132 @@
1
1
  # IrisMail
2
2
 
3
- A modular, secure, and easy-to-use Email and OTP service for Next.js applications, with built-in Shadcn UI support.
3
+ A lightweight npm package for sending emails via Gmail and beautiful OTP input components for React.
4
4
 
5
5
  ## Features
6
6
 
7
- - 📧 **Email Service**: Simple wrapper around Nodemailer for sending emails.
8
- - 🔐 **Secure OTP**: Server-side OTP generation, encryption, and validation.
9
- - 🎨 **Shadcn UI**: Ready-to-use OTP Input component compatible with Shadcn UI.
10
- - **React Hook**: `useOTP` hook for managing timers, rate limiting, and resend cooldowns.
11
- - 🛡️ **Type-Safe**: Built with TypeScript.
7
+ - 📧 **Email Service**: Simple Gmail-based email sending with minimal configuration
8
+ - 🔢 **OTP Input**: Beautiful, accessible OTP input components with copy-paste support
9
+ - **Easy to Use**: Minimal API surface - just a few props to get started
10
+ - 🛡️ **Type-Safe**: Built with TypeScript
11
+ - ⚙️ **Zero Config**: Just add your Gmail credentials - no SMTP configuration needed
12
12
 
13
13
  ## Installation
14
14
 
15
15
  ```bash
16
16
  npm install irismail
17
- # or
18
- yarn add irismail
19
- # or
20
- pnpm add irismail
21
17
  ```
22
18
 
23
- ## Usage
19
+ ## Email Service
24
20
 
25
- ### 1. Server-Side Setup (API Routes)
26
-
27
- Initialize the service with your email configuration and a secret key for encryption.
21
+ Send emails with just your Gmail credentials:
28
22
 
29
23
  ```typescript
30
- // app/api/send-otp/route.ts
31
- import { IrisMailService } from 'irismail/server';
32
- import { NextResponse } from 'next/server';
33
-
34
- const irismail = new IrisMailService({
35
- email: {
36
- transport: {
37
- host: 'smtp.gmail.com',
38
- port: 587,
39
- auth: {
40
- user: process.env.GMAIL_USER,
41
- pass: process.env.GMAIL_PASS,
42
- },
43
- },
44
- defaults: {
45
- from: {
46
- name: 'My App',
47
- address: process.env.GMAIL_USER,
48
- },
49
- },
24
+ import { IrisMail } from 'irismail/server';
25
+
26
+ const mail = new IrisMail({
27
+ auth: {
28
+ user: process.env.GMAIL_USER!,
29
+ pass: process.env.GMAIL_APP_PASSWORD!,
50
30
  },
51
- secretKey: process.env.OTP_SECRET_KEY, // Must be 32 chars
52
31
  });
53
32
 
54
- export async function POST(req: Request) {
55
- const { email } = await req.json();
56
-
57
- // Generate and encrypt OTP
58
- const otp = irismail.otp.generateOTP();
59
- const encryptedOtp = irismail.otp.encryptOTP(otp);
60
-
61
- // Send email
62
- await irismail.email.sendEmail({
63
- to: email,
64
- subject: 'Your Verification Code',
65
- html: `<p>Your code is: <b>${otp}</b></p>`,
66
- });
67
-
68
- return NextResponse.json({ encryptedOtp });
69
- }
33
+ await mail.sendMail({
34
+ from: process.env.GMAIL_USER!,
35
+ to: 'user@example.com',
36
+ subject: 'Hello!',
37
+ html: '<h1>Welcome</h1><p>Thanks for signing up!</p>',
38
+ });
70
39
  ```
71
40
 
72
- ### 2. Client-Side Setup (React Component)
41
+ > **Note:** You'll need to generate a [Gmail App Password](https://support.google.com/accounts/answer/185833) for authentication.
73
42
 
74
- Use the `InputOTP` component and `useOTP` hook in your verification form.
43
+ ## OTP Input Component
44
+
45
+ ### Basic Usage
75
46
 
76
47
  ```tsx
77
- // components/verify-form.tsx
78
48
  'use client';
79
49
 
80
50
  import { useState } from 'react';
81
- import { InputOTP, InputOTPGroup, InputOTPSlot, useOTP } from 'irismail/react';
51
+ import { OTP } from 'irismail/react';
82
52
 
83
- export function VerifyForm({ email }) {
84
- const [otp, setOtp] = useState('');
85
- const { otpTimeLeft, resendTimeLeft, formatTime } = useOTP({ email });
53
+ export function VerifyForm() {
54
+ const [code, setCode] = useState('');
86
55
 
87
56
  return (
88
- <div className="space-y-4">
89
- <InputOTP
90
- maxLength={6}
91
- value={otp}
92
- onChange={setOtp}
93
- >
94
- <InputOTPGroup>
95
- <InputOTPSlot index={0} />
96
- <InputOTPSlot index={1} />
97
- <InputOTPSlot index={2} />
98
- <InputOTPSlot index={3} />
99
- <InputOTPSlot index={4} />
100
- <InputOTPSlot index={5} />
101
- </InputOTPGroup>
102
- </InputOTP>
103
-
104
- <div className="text-sm">
105
- Time remaining: {formatTime(otpTimeLeft)}
106
- </div>
107
- </div>
57
+ <OTP
58
+ value={code}
59
+ onChange={setCode}
60
+ onComplete={(value) => verify(value)}
61
+ />
108
62
  );
109
63
  }
110
64
  ```
111
65
 
112
- ## Local Testing
66
+ ### With Error State
67
+
68
+ ```tsx
69
+ <OTP
70
+ value={code}
71
+ onChange={setCode}
72
+ error={isInvalid}
73
+ />
74
+ ```
113
75
 
114
- Want to test the package locally before publishing? We've included a complete example app!
76
+ ### Custom Length
115
77
 
116
- 1. **Navigate to the example directory**:
117
- ```bash
118
- cd example
119
- ```
78
+ ```tsx
79
+ <OTP length={4} value={code} onChange={setCode} />
80
+ ```
120
81
 
121
- 2. **Set up environment variables**:
122
- ```bash
123
- cp .env.example .env.local
124
- # Edit .env.local with your Gmail credentials and secret key
125
- ```
82
+ ### Composition Pattern
126
83
 
127
- 3. **Run the development server**:
128
- ```bash
129
- npm run dev
130
- ```
84
+ For custom layouts with separators:
131
85
 
132
- 4. **Open [http://localhost:3000](http://localhost:3000)** to see:
133
- - Interactive OTP demo with email sending
134
- - Live documentation with code examples
135
- - All features working together
86
+ ```tsx
87
+ import {
88
+ InputOTP,
89
+ InputOTPGroup,
90
+ InputOTPSlot,
91
+ InputOTPSeparator
92
+ } from 'irismail/react';
93
+
94
+ <InputOTP maxLength={6} value={code} onChange={setCode}>
95
+ <InputOTPGroup>
96
+ <InputOTPSlot index={0} />
97
+ <InputOTPSlot index={1} />
98
+ <InputOTPSlot index={2} />
99
+ </InputOTPGroup>
100
+ <InputOTPSeparator />
101
+ <InputOTPGroup>
102
+ <InputOTPSlot index={3} />
103
+ <InputOTPSlot index={4} />
104
+ <InputOTPSlot index={5} />
105
+ </InputOTPGroup>
106
+ </InputOTP>
107
+ ```
136
108
 
137
- See `example/README.md` for more details.
109
+ ## OTP Props
138
110
 
139
- ## Publishing to NPM
111
+ | Prop | Type | Default | Description |
112
+ |------|------|---------|-------------|
113
+ | `length` | number | 6 | Number of OTP digits |
114
+ | `value` | string | — | Controlled input value |
115
+ | `onChange` | (value: string) => void | — | Called when value changes |
116
+ | `onComplete` | (value: string) => void | — | Called when all digits entered |
117
+ | `disabled` | boolean | false | Disable the input |
118
+ | `error` | boolean | false | Show error styling |
119
+ | `autoFocus` | boolean | false | Auto focus first slot |
120
+ | `name` | string | — | Name attribute for forms |
121
+ | `className` | string | — | Container className |
140
122
 
141
- 1. **Login to NPM**:
142
- ```bash
143
- npm login
144
- ```
123
+ ## Documentation
145
124
 
146
- 2. **Build the package**:
147
- ```bash
148
- npm run build
149
- ```
125
+ For full documentation and interactive examples, run the docs site:
150
126
 
151
- 3. **Publish**:
152
- ```bash
153
- npm publish --access public
154
- ```
127
+ ```bash
128
+ npm run site
129
+ ```
155
130
 
156
131
  ## License
157
132
 
@@ -0,0 +1,42 @@
1
+ import {
2
+ __publicField
3
+ } from "./chunk-QZ7TP4HQ.mjs";
4
+
5
+ // src/server/email.ts
6
+ import nodemailer from "nodemailer";
7
+ var IrisMail = class {
8
+ constructor(config) {
9
+ __publicField(this, "transporter");
10
+ this.transporter = nodemailer.createTransport({
11
+ host: "smtp.gmail.com",
12
+ port: 587,
13
+ secure: false,
14
+ // true for 465, false for other ports
15
+ auth: config.auth
16
+ });
17
+ }
18
+ /**
19
+ * Send an email
20
+ * @param options - Email options (from, to, subject, html)
21
+ * @returns Promise with success status and messageId
22
+ */
23
+ async sendMail(options) {
24
+ const { from, to, subject, html } = options;
25
+ const text = html.replace(/<[^>]*>/g, "");
26
+ const info = await this.transporter.sendMail({
27
+ from,
28
+ to,
29
+ subject,
30
+ html,
31
+ text
32
+ });
33
+ return {
34
+ success: true,
35
+ messageId: info.messageId
36
+ };
37
+ }
38
+ };
39
+
40
+ export {
41
+ IrisMail
42
+ };
@@ -0,0 +1,127 @@
1
+ // src/react/components/input-otp.tsx
2
+ import { OTPInput, OTPInputContext } from "input-otp";
3
+ import * as React from "react";
4
+
5
+ // src/utils/constants.ts
6
+ import { clsx } from "clsx";
7
+ import { twMerge } from "tailwind-merge";
8
+ function cn(...inputs) {
9
+ return twMerge(clsx(inputs));
10
+ }
11
+
12
+ // src/react/components/input-otp.tsx
13
+ import { jsx, jsxs } from "react/jsx-runtime";
14
+ var InputOTPStyleContext = React.createContext({});
15
+ var InputOTP = React.forwardRef(({ className, containerClassName, error, ...props }, ref) => /* @__PURE__ */ jsx(InputOTPStyleContext.Provider, { value: { error }, children: /* @__PURE__ */ jsx(
16
+ OTPInput,
17
+ {
18
+ ref,
19
+ containerClassName: cn(
20
+ "flex items-center gap-2 has-[:disabled]:opacity-50",
21
+ containerClassName
22
+ ),
23
+ className: cn("disabled:cursor-not-allowed", className),
24
+ ...props
25
+ }
26
+ ) }));
27
+ InputOTP.displayName = "InputOTP";
28
+ var InputOTPGroup = React.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
29
+ "div",
30
+ {
31
+ ref,
32
+ className: cn("flex items-center gap-2", className),
33
+ ...props
34
+ }
35
+ ));
36
+ InputOTPGroup.displayName = "InputOTPGroup";
37
+ var InputOTPSlot = React.forwardRef(({ index, className, ...props }, ref) => {
38
+ const inputOTPContext = React.useContext(OTPInputContext);
39
+ const { error } = React.useContext(InputOTPStyleContext);
40
+ const slot = inputOTPContext.slots[index];
41
+ const { char, hasFakeCaret, isActive } = slot;
42
+ return /* @__PURE__ */ jsxs(
43
+ "div",
44
+ {
45
+ ref,
46
+ className: cn(
47
+ // Base styles
48
+ "relative flex h-12 w-10 items-center justify-center",
49
+ "border-2 rounded-lg",
50
+ "font-mono text-lg font-medium",
51
+ "transition-all duration-150",
52
+ // Default state
53
+ "border-zinc-700 bg-zinc-900 text-white",
54
+ // Filled state
55
+ char && "border-zinc-500",
56
+ // Active/focus state
57
+ isActive && "ring-2 ring-offset-2 ring-offset-zinc-950 ring-indigo-500 border-indigo-500",
58
+ // Error state
59
+ error && "border-red-500/70 bg-red-500/10",
60
+ error && isActive && "ring-red-500",
61
+ className
62
+ ),
63
+ "data-active": isActive,
64
+ "data-filled": Boolean(char),
65
+ ...props,
66
+ children: [
67
+ char,
68
+ hasFakeCaret && /* @__PURE__ */ jsx("div", { className: "pointer-events-none absolute inset-0 flex items-center justify-center", children: /* @__PURE__ */ jsx("div", { className: "h-5 w-0.5 animate-caret-blink rounded-full bg-indigo-400" }) })
69
+ ]
70
+ }
71
+ );
72
+ });
73
+ InputOTPSlot.displayName = "InputOTPSlot";
74
+ var InputOTPSeparator = React.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
75
+ "div",
76
+ {
77
+ ref,
78
+ role: "separator",
79
+ className: cn("flex items-center justify-center text-zinc-500", className),
80
+ ...props,
81
+ children: /* @__PURE__ */ jsx("span", { className: "w-3 h-0.5 bg-zinc-600 rounded-full" })
82
+ }
83
+ ));
84
+ InputOTPSeparator.displayName = "InputOTPSeparator";
85
+ var OTP = React.forwardRef(
86
+ ({
87
+ length = 6,
88
+ value,
89
+ onChange,
90
+ onComplete,
91
+ disabled,
92
+ error,
93
+ autoFocus,
94
+ name,
95
+ className,
96
+ pattern = "^[0-9]*$"
97
+ }, ref) => {
98
+ const slotIndices = Array.from({ length }, (_, i) => i);
99
+ return /* @__PURE__ */ jsx(
100
+ InputOTP,
101
+ {
102
+ ref,
103
+ maxLength: length,
104
+ value,
105
+ onChange,
106
+ onComplete,
107
+ disabled,
108
+ error,
109
+ autoFocus,
110
+ name,
111
+ pattern,
112
+ containerClassName: className,
113
+ children: /* @__PURE__ */ jsx(InputOTPGroup, { children: slotIndices.map((index) => /* @__PURE__ */ jsx(InputOTPSlot, { index }, index)) })
114
+ }
115
+ );
116
+ }
117
+ );
118
+ OTP.displayName = "OTP";
119
+
120
+ export {
121
+ cn,
122
+ InputOTP,
123
+ InputOTPGroup,
124
+ InputOTPSlot,
125
+ InputOTPSeparator,
126
+ OTP
127
+ };
package/dist/index.d.mts CHANGED
@@ -1,16 +1,9 @@
1
- export { EmailConfig, EmailService, IrisMailService, OTPData, OTPService, SendEmailOptions, decrypt, encrypt } from './server/index.mjs';
2
- export { InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot, useOTP } from './react/index.mjs';
1
+ export { IrisMail, IrisMailConfig, SendMailOptions, SendMailResult } from './server/index.mjs';
2
+ export { InputOTP, InputOTPGroup, InputOTPProps, InputOTPSeparator, InputOTPSlot, OTP, OTPProps } from './react/index.mjs';
3
3
  import { ClassValue } from 'clsx';
4
4
  import 'input-otp';
5
5
  import 'react';
6
6
 
7
7
  declare function cn(...inputs: ClassValue[]): string;
8
- declare const OTP_DEFAULTS: {
9
- LENGTH: number;
10
- EXPIRY_MINUTES: number;
11
- RESEND_COOLDOWN_SECONDS: number;
12
- MAX_ATTEMPTS: number;
13
- RATE_LIMIT_RESET_HOURS: number;
14
- };
15
8
 
16
- export { OTP_DEFAULTS, cn };
9
+ export { cn };
package/dist/index.d.ts CHANGED
@@ -1,16 +1,9 @@
1
- export { EmailConfig, EmailService, IrisMailService, OTPData, OTPService, SendEmailOptions, decrypt, encrypt } from './server/index.js';
2
- export { InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot, useOTP } from './react/index.js';
1
+ export { IrisMail, IrisMailConfig, SendMailOptions, SendMailResult } from './server/index.js';
2
+ export { InputOTP, InputOTPGroup, InputOTPProps, InputOTPSeparator, InputOTPSlot, OTP, OTPProps } from './react/index.js';
3
3
  import { ClassValue } from 'clsx';
4
4
  import 'input-otp';
5
5
  import 'react';
6
6
 
7
7
  declare function cn(...inputs: ClassValue[]): string;
8
- declare const OTP_DEFAULTS: {
9
- LENGTH: number;
10
- EXPIRY_MINUTES: number;
11
- RESEND_COOLDOWN_SECONDS: number;
12
- MAX_ATTEMPTS: number;
13
- RATE_LIMIT_RESET_HOURS: number;
14
- };
15
8
 
16
- export { OTP_DEFAULTS, cn };
9
+ export { cn };