Appearance
Popup Authentication
A complete popup with email and Google OAuth authentication.
Overview
This example shows how to build a professional authentication popup with:
- Email-based login
- Google OAuth sign-in
- Session persistence
- Loading states
- Error handling
Project Structure
auth-extension/
├── manifest.json
├── popup/
│ ├── popup.html
│ ├── popup.js
│ └── popup.css
├── icons/
│ ├── google.svg
│ └── icon128.png
└── package.jsonManifest Configuration
json
{
"manifest_version": 3,
"name": "Auth Extension",
"version": "1.0.0",
"description": "Extension with Google OAuth",
"action": {
"default_popup": "popup/popup.html",
"default_icon": "icons/icon128.png"
},
"permissions": [
"identity",
"storage"
],
"host_permissions": [
"https://api.extensionlogin.com/*"
],
"oauth2": {
"client_id": "YOUR_GOOGLE_CLIENT_ID.apps.googleusercontent.com",
"scopes": [
"https://www.googleapis.com/auth/userinfo.email",
"https://www.googleapis.com/auth/userinfo.profile"
]
}
}HTML Structure
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Auth Extension</title>
<link rel="stylesheet" href="popup.css">
</head>
<body>
<!-- Loading State -->
<div id="loading-view" class="view">
<div class="spinner"></div>
</div>
<!-- Login View -->
<div id="login-view" class="view hidden">
<div class="header">
<img src="../icons/icon128.png" alt="Logo" class="logo">
<h1>Welcome</h1>
<p class="subtitle">Sign in to continue</p>
</div>
<div class="auth-options">
<button id="google-btn" class="btn btn-google">
<img src="../icons/google.svg" alt="Google">
Continue with Google
</button>
<div class="divider">
<span>or</span>
</div>
<form id="email-form">
<div class="form-group">
<input
type="email"
id="email"
placeholder="Email address"
required
>
</div>
<div class="form-group">
<input
type="text"
id="name"
placeholder="Your name (optional)"
>
</div>
<button type="submit" class="btn btn-primary">
Continue with Email
</button>
</form>
</div>
<div id="error-message" class="error hidden"></div>
</div>
<!-- Dashboard View -->
<div id="dashboard-view" class="view hidden">
<div class="user-header">
<img id="user-avatar" src="" alt="Avatar" class="avatar">
<div class="user-info">
<h2 id="user-name"></h2>
<p id="user-email"></p>
</div>
</div>
<div class="dashboard-content">
<div class="stat-card">
<span class="stat-label">Status</span>
<span class="stat-value">Active</span>
</div>
<div class="stat-card">
<span class="stat-label">Member Since</span>
<span id="member-since" class="stat-value"></span>
</div>
</div>
<button id="logout-btn" class="btn btn-secondary">
Sign Out
</button>
</div>
<script type="module" src="popup.js"></script>
</body>
</html>CSS Styling
css
/* popup.css */
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
width: 360px;
min-height: 400px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #1a1a1a;
}
.view {
padding: 24px;
background: white;
min-height: 400px;
display: flex;
flex-direction: column;
}
.hidden {
display: none !important;
}
/* Loading */
#loading-view {
justify-content: center;
align-items: center;
}
.spinner {
width: 40px;
height: 40px;
border: 3px solid #e5e7eb;
border-top-color: #3b82f6;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* Header */
.header {
text-align: center;
margin-bottom: 24px;
}
.logo {
width: 64px;
height: 64px;
margin-bottom: 16px;
}
.header h1 {
font-size: 24px;
font-weight: 600;
color: #1a1a1a;
}
.subtitle {
color: #6b7280;
margin-top: 4px;
}
/* Auth Options */
.auth-options {
flex: 1;
}
.btn {
width: 100%;
padding: 12px 16px;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
transition: all 0.2s;
}
.btn-google {
background: white;
border: 1px solid #e5e7eb;
color: #374151;
}
.btn-google:hover {
background: #f9fafb;
border-color: #d1d5db;
}
.btn-google img {
width: 18px;
height: 18px;
}
.btn-primary {
background: #3b82f6;
border: none;
color: white;
}
.btn-primary:hover {
background: #2563eb;
}
.btn-primary:disabled {
background: #9ca3af;
cursor: not-allowed;
}
.btn-secondary {
background: #f3f4f6;
border: none;
color: #374151;
}
.btn-secondary:hover {
background: #e5e7eb;
}
/* Divider */
.divider {
display: flex;
align-items: center;
margin: 20px 0;
}
.divider::before,
.divider::after {
content: '';
flex: 1;
height: 1px;
background: #e5e7eb;
}
.divider span {
padding: 0 12px;
color: #9ca3af;
font-size: 12px;
}
/* Form */
.form-group {
margin-bottom: 12px;
}
input {
width: 100%;
padding: 12px;
border: 1px solid #e5e7eb;
border-radius: 8px;
font-size: 14px;
transition: border-color 0.2s;
}
input:focus {
outline: none;
border-color: #3b82f6;
}
input::placeholder {
color: #9ca3af;
}
/* Error */
.error {
margin-top: 16px;
padding: 12px;
background: #fee2e2;
border-radius: 8px;
color: #991b1b;
font-size: 14px;
text-align: center;
}
/* Dashboard */
.user-header {
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 24px;
}
.avatar {
width: 56px;
height: 56px;
border-radius: 50%;
background: #e5e7eb;
}
.user-info h2 {
font-size: 18px;
font-weight: 600;
}
.user-info p {
color: #6b7280;
font-size: 14px;
}
.dashboard-content {
flex: 1;
}
.stat-card {
display: flex;
justify-content: space-between;
padding: 16px;
background: #f9fafb;
border-radius: 8px;
margin-bottom: 12px;
}
.stat-label {
color: #6b7280;
font-size: 14px;
}
.stat-value {
font-weight: 500;
color: #1a1a1a;
}JavaScript Implementation
javascript
// popup.js
import ExtensionLogin from 'extensionlogin';
// Initialize
ExtensionLogin.init({
apiKey: 'el_live_your_api_key_here',
autoIdentify: true,
debug: true
});
// Views
const loadingView = document.getElementById('loading-view');
const loginView = document.getElementById('login-view');
const dashboardView = document.getElementById('dashboard-view');
// Login Elements
const googleBtn = document.getElementById('google-btn');
const emailForm = document.getElementById('email-form');
const emailInput = document.getElementById('email');
const nameInput = document.getElementById('name');
const errorMessage = document.getElementById('error-message');
// Dashboard Elements
const userAvatar = document.getElementById('user-avatar');
const userName = document.getElementById('user-name');
const userEmail = document.getElementById('user-email');
const memberSince = document.getElementById('member-since');
const logoutBtn = document.getElementById('logout-btn');
// Check for existing user
async function init() {
const user = ExtensionLogin.getUser();
if (user) {
showDashboard(user);
} else {
showLogin();
}
}
// Google Sign-In
googleBtn.addEventListener('click', async () => {
googleBtn.disabled = true;
googleBtn.innerHTML = '<span class="spinner" style="width:20px;height:20px;"></span> Signing in...';
hideError();
try {
const result = await ExtensionLogin.loginWithGoogle();
if (result.success) {
showDashboard(result.user);
} else {
showError(getErrorMessage(result.error.code));
}
} catch (error) {
showError('Failed to connect. Please try again.');
} finally {
googleBtn.disabled = false;
googleBtn.innerHTML = '<img src="../icons/google.svg" alt="Google"> Continue with Google';
}
});
// Email Sign-In
emailForm.addEventListener('submit', async (e) => {
e.preventDefault();
const email = emailInput.value.trim();
const name = nameInput.value.trim();
if (!email) {
showError('Please enter your email address');
return;
}
const submitBtn = emailForm.querySelector('button[type="submit"]');
submitBtn.disabled = true;
submitBtn.textContent = 'Signing in...';
hideError();
try {
const result = await ExtensionLogin.identify({
email,
name: name || undefined
});
if (result.success) {
showDashboard(result.user);
} else {
showError(getErrorMessage(result.error.code));
}
} catch (error) {
showError('Failed to connect. Please try again.');
} finally {
submitBtn.disabled = false;
submitBtn.textContent = 'Continue with Email';
}
});
// Logout
logoutBtn.addEventListener('click', async () => {
logoutBtn.disabled = true;
await ExtensionLogin.logout();
showLogin();
logoutBtn.disabled = false;
});
// View Helpers
function showLoading() {
loadingView.classList.remove('hidden');
loginView.classList.add('hidden');
dashboardView.classList.add('hidden');
}
function showLogin() {
loadingView.classList.add('hidden');
loginView.classList.remove('hidden');
dashboardView.classList.add('hidden');
// Clear form
emailInput.value = '';
nameInput.value = '';
hideError();
}
function showDashboard(user) {
loadingView.classList.add('hidden');
loginView.classList.add('hidden');
dashboardView.classList.remove('hidden');
// Set user info
userName.textContent = user.name || 'User';
userEmail.textContent = user.email;
// Set avatar
if (user.picture) {
userAvatar.src = user.picture;
} else {
// Generate initials avatar
const initials = (user.name || user.email)
.split(' ')
.map(n => n[0])
.join('')
.toUpperCase()
.slice(0, 2);
userAvatar.src = `https://ui-avatars.com/api/?name=${initials}&background=3b82f6&color=fff`;
}
// Set member since
const date = new Date(user.createdAt);
memberSince.textContent = date.toLocaleDateString('en-US', {
month: 'short',
year: 'numeric'
});
}
// Error Helpers
function showError(message) {
errorMessage.textContent = message;
errorMessage.classList.remove('hidden');
}
function hideError() {
errorMessage.classList.add('hidden');
}
function getErrorMessage(code) {
const messages = {
'INVALID_EMAIL': 'Please enter a valid email address.',
'USER_CANCELLED': 'Sign-in was cancelled.',
'OAUTH_NOT_CONFIGURED': 'Google sign-in is not available.',
'RATE_LIMITED': 'Too many attempts. Please wait a moment.',
'NETWORK_ERROR': 'Connection error. Please check your internet.'
};
return messages[code] || 'Something went wrong. Please try again.';
}
// Listen for auth changes
ExtensionLogin.onAuthChange((user) => {
if (user) {
showDashboard(user);
} else {
showLogin();
}
});
// Initialize
init();Google Icon SVG
Create icons/google.svg:
xml
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/>
<path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/>
<path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/>
<path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/>
</svg>Testing the Extension
Build your extension:
bashnpm run buildLoad in Chrome:
- Go to
chrome://extensions - Enable "Developer mode"
- Click "Load unpacked"
- Select your build folder
- Go to
Test both authentication methods:
- Try email sign-in
- Try Google sign-in
- Verify logout works
- Check session persists after closing popup
Next Steps
- Background Script - Handle auth in service workers
- Content Script - Access user in content scripts
- CRM Integration - Send users to your CRM