@haloduck/ui 2.0.46 → 2.0.48

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.
@@ -1,6 +1,6 @@
1
1
  import * as i0 from '@angular/core';
2
2
  import { Injectable, Input, Component, inject, ChangeDetectorRef, forwardRef, ViewChild, signal, EventEmitter, Output, ViewContainerRef, NgZone, isDevMode, ElementRef, Directive, ChangeDetectionStrategy, HostBinding } from '@angular/core';
3
- import { signIn, confirmSignIn, resetPassword, confirmResetPassword } from 'aws-amplify/auth';
3
+ import { signIn, confirmSignIn, resetPassword, confirmResetPassword, fetchAuthSession } from 'aws-amplify/auth';
4
4
  import * as i1$1 from '@angular/forms';
5
5
  import { NG_VALUE_ACCESSOR, Validators, FormsModule, ReactiveFormsModule } from '@angular/forms';
6
6
  import { BehaviorSubject, zip, Subject, takeUntil, tap, combineLatest, switchMap, of, Observable, distinctUntilChanged, shareReplay, map, take } from 'rxjs';
@@ -10,13 +10,14 @@ import { CommonModule, DecimalPipe, DatePipe } from '@angular/common';
10
10
  import * as i2$1 from '@jsverse/transloco';
11
11
  import { provideTranslocoScope, TranslocoModule, TranslocoService } from '@jsverse/transloco';
12
12
  import * as i2 from '@angular/common/http';
13
+ import * as i1$4 from '@angular/router';
14
+ import { ActivatedRoute, Router, NavigationEnd, RouterLink } from '@angular/router';
15
+ import { Hub } from 'aws-amplify/utils';
13
16
  import * as i1$2 from '@angular/cdk/overlay';
14
17
  import { Overlay } from '@angular/cdk/overlay';
15
18
  import { ComponentPortal } from '@angular/cdk/portal';
16
19
  import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
17
20
  import { CoreService } from '@haloduck/core';
18
- import * as i1$4 from '@angular/router';
19
- import { Router, NavigationEnd, RouterLink } from '@angular/router';
20
21
  import * as i1$3 from '@angular/platform-browser';
21
22
  import { Title } from '@angular/platform-browser';
22
23
  import * as THREE from 'three';
@@ -199,6 +200,14 @@ class AuthenticateComponent {
199
200
  fb;
200
201
  http;
201
202
  notificationService = inject(NotificationService);
203
+ metaData;
204
+ socialLoginProviders = [
205
+ { name: 'google', enabled: true },
206
+ { name: 'apple', enabled: true },
207
+ { name: 'kakao', enabled: true },
208
+ ];
209
+ showSocialLogin = true;
210
+ showDivider = true;
202
211
  loginForm;
203
212
  signupForm;
204
213
  resetForm;
@@ -237,6 +246,7 @@ class AuthenticateComponent {
237
246
  password: this.loginForm.value.password,
238
247
  options: {
239
248
  authFlowType: 'USER_PASSWORD_AUTH',
249
+ clientMetadata: this.metaData,
240
250
  },
241
251
  })
242
252
  .then((res) => {
@@ -261,6 +271,9 @@ class AuthenticateComponent {
261
271
  confirmNewPassword() {
262
272
  confirmSignIn({
263
273
  challengeResponse: this.newPasswordForm.value.newPassword,
274
+ options: {
275
+ clientMetadata: this.metaData,
276
+ },
264
277
  })
265
278
  .then(() => {
266
279
  this.notificationService.showNotification('success', 'Successfully signed in.', 3000);
@@ -281,7 +294,10 @@ class AuthenticateComponent {
281
294
  this.http.post('/api/signup', this.signupForm.value).subscribe(console.log);
282
295
  }
283
296
  reset() {
284
- resetPassword({ username: this.resetForm.value.email })
297
+ resetPassword({
298
+ username: this.resetForm.value.email,
299
+ options: { clientMetadata: this.metaData },
300
+ })
285
301
  .then(() => {
286
302
  this.switchStage('otpVerify');
287
303
  })
@@ -294,6 +310,7 @@ class AuthenticateComponent {
294
310
  username: this.resetForm.value.email,
295
311
  confirmationCode: this.otpForm.value.code,
296
312
  newPassword: this.otpForm.value.newPassword,
313
+ options: { clientMetadata: this.metaData },
297
314
  })
298
315
  .then(() => {
299
316
  this.notificationService.showNotification('success', 'Successfully reset password.', 3000);
@@ -337,13 +354,432 @@ class AuthenticateComponent {
337
354
  { label: 'one special character', valid: this.hasSpecialChar },
338
355
  ];
339
356
  }
357
+ // Social Login Methods
358
+ get enabledSocialProviders() {
359
+ return this.socialLoginProviders.filter((provider) => provider.enabled);
360
+ }
361
+ isProviderEnabled(providerName) {
362
+ return this.socialLoginProviders.some((provider) => provider.name === providerName && provider.enabled);
363
+ }
364
+ loginWithGoogle() {
365
+ try {
366
+ // Google 로그인 로직 구현
367
+ // 실제 구현시에는 Google OAuth2 SDK 또는 AWS Amplify의 Google 제공자를 사용
368
+ console.log('Google login initiated');
369
+ // 예시: window.location을 사용한 OAuth 리다이렉션
370
+ const googleProvider = this.socialLoginProviders.find((p) => p.name === 'google');
371
+ if (googleProvider?.clientId) {
372
+ const redirectUrl = googleProvider.redirectUrl || window.location.origin;
373
+ const googleAuthUrl = `https://accounts.google.com/oauth/authorize?client_id=${googleProvider.clientId}&redirect_uri=${redirectUrl}&scope=openid%20email%20profile&response_type=code`;
374
+ window.location.href = googleAuthUrl;
375
+ }
376
+ else {
377
+ this.notificationService.showNotification('error', 'Google login configuration missing.');
378
+ }
379
+ }
380
+ catch (error) {
381
+ this.notificationService.showNotification('error', 'Google login failed.');
382
+ }
383
+ }
384
+ loginWithApple() {
385
+ try {
386
+ // Apple 로그인 로직 구현
387
+ // 실제 구현시에는 Apple Sign In JS SDK를 사용
388
+ console.log('Apple login initiated');
389
+ const appleProvider = this.socialLoginProviders.find((p) => p.name === 'apple');
390
+ if (appleProvider?.clientId) {
391
+ const redirectUrl = appleProvider.redirectUrl || window.location.origin;
392
+ const appleAuthUrl = `https://appleid.apple.com/auth/authorize?client_id=${appleProvider.clientId}&redirect_uri=${redirectUrl}&scope=name%20email&response_type=code&response_mode=form_post`;
393
+ window.location.href = appleAuthUrl;
394
+ }
395
+ else {
396
+ this.notificationService.showNotification('error', 'Apple login configuration missing.');
397
+ }
398
+ }
399
+ catch (error) {
400
+ this.notificationService.showNotification('error', 'Apple login failed.');
401
+ }
402
+ }
403
+ loginWithKakao() {
404
+ try {
405
+ // KakaoTalk 로그인 로직 구현
406
+ // 실제 구현시에는 Kakao JavaScript SDK를 사용
407
+ console.log('Kakao login initiated');
408
+ const kakaoProvider = this.socialLoginProviders.find((p) => p.name === 'kakao');
409
+ if (kakaoProvider?.clientId) {
410
+ const redirectUrl = kakaoProvider.redirectUrl || window.location.origin;
411
+ const kakaoAuthUrl = `https://kauth.kakao.com/oauth/authorize?client_id=${kakaoProvider.clientId}&redirect_uri=${redirectUrl}&response_type=code&state=${encodeURIComponent(JSON.stringify(kakaoProvider.state || {}))}`;
412
+ window.location.href = kakaoAuthUrl;
413
+ }
414
+ else {
415
+ this.notificationService.showNotification('error', 'Kakao login configuration missing.');
416
+ }
417
+ }
418
+ catch (error) {
419
+ this.notificationService.showNotification('error', 'Kakao login failed.');
420
+ }
421
+ }
422
+ getSocialLoginIcon(provider) {
423
+ const icons = {
424
+ google: '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 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 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 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',
425
+ apple: 'M17.05 20.28c-.98.95-2.05.8-3.08.35-1.09-.46-2.09-.48-3.24 0-1.44.62-2.2.44-3.06-.35C2.79 15.25 3.51 7.59 9.05 7.31c1.35.07 2.29.74 3.08.8 1.18-.24 2.31-.93 3.57-.84 1.51.12 2.65.72 3.4 1.8-3.12 1.87-2.38 5.98.48 7.13-.57 1.5-1.31 2.99-2.54 4.09l.01-.01zM12.03 7.25c-.15-2.23 1.66-4.07 3.74-4.25.29 2.58-2.34 4.5-3.74 4.25z',
426
+ kakao: 'M12 3C7.03 3 3 6.17 3 10.08c0 2.53 1.67 4.74 4.19 6.08L6.45 18.9c-.14.22-.02.52.24.52.08 0 .17-.03.23-.09L9.2 17.4c.87.17 1.8.26 2.8.26s1.93-.09 2.8-.26l2.28 1.93c.06.06.15.09.23.09.26 0 .38-.3.24-.52l-.74-2.74C19.33 14.82 21 12.61 21 10.08 21 6.17 16.97 3 12 3z',
427
+ };
428
+ return icons[provider] || '';
429
+ }
430
+ getSocialLoginLabel(provider) {
431
+ const labels = {
432
+ google: 'Continue with Google',
433
+ apple: 'Continue with Apple',
434
+ kakao: 'Continue with KakaoTalk',
435
+ };
436
+ return labels[provider] || `Continue with ${provider}`;
437
+ }
340
438
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: AuthenticateComponent, deps: [{ token: i1$1.FormBuilder }, { token: i2.HttpClient }], target: i0.ɵɵFactoryTarget.Component });
341
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.1.4", type: AuthenticateComponent, isStandalone: true, selector: "haloduck-authenticate", ngImport: i0, template: "<div class=\"flex flex-col items-center justify-center h-full\">\n <!-- auth.component.html -->\n <div\n class=\"w-full max-w-md mx-auto mt-20 p-8 shadow-lg rounded-2xl bg-light-background dark:bg-dark-background\"\n >\n @switch (stage) {\n @case ('login') {\n <!-- \uB85C\uADF8\uC778 -->\n <form [formGroup]=\"loginForm\" (ngSubmit)=\"login()\" class=\"flex flex-col gap-4\">\n <haloduck-input\n data-testid=\"authenticate-login-email\"\n type=\"email\"\n formControlName=\"email\"\n placeholder=\"Email\"\n />\n <haloduck-input\n data-testid=\"authenticate-login-password\"\n type=\"password\"\n formControlName=\"password\"\n placeholder=\"Password\"\n />\n <haloduck-button\n data-testid=\"authenticate-login-submit\"\n type=\"submit\"\n variant=\"primary\"\n (click)=\"login()\"\n >Sign In</haloduck-button\n >\n </form>\n }\n @case ('signup') {\n <!-- \uD68C\uC6D0\uAC00\uC785 -->\n <form [formGroup]=\"signupForm\" (ngSubmit)=\"signup()\" class=\"flex flex-col gap-4\">\n <haloduck-input\n data-testid=\"authenticate-signup-email\"\n type=\"email\"\n formControlName=\"email\"\n placeholder=\"Email\"\n />\n <haloduck-input\n data-testid=\"authenticate-signup-password\"\n type=\"password\"\n formControlName=\"password\"\n placeholder=\"Password\"\n />\n <haloduck-button data-testid=\"authenticate-signup-submit\" type=\"submit\" variant=\"primary\"\n >Sign Up</haloduck-button\n >\n </form>\n }\n @case ('reset') {\n <!-- \uBE44\uBC00\uBC88\uD638 \uC7AC\uC124\uC815 \uC694\uCCAD -->\n <form [formGroup]=\"resetForm\" (ngSubmit)=\"reset()\" class=\"flex flex-col gap-4\">\n <haloduck-input\n data-testid=\"authenticate-reset-email\"\n type=\"email\"\n formControlName=\"email\"\n placeholder=\"Email\"\n />\n <haloduck-button\n data-testid=\"authenticate-reset-submit\"\n type=\"submit\"\n variant=\"primary\"\n (click)=\"reset()\"\n >Send Verification Code</haloduck-button\n >\n </form>\n }\n @case ('otpVerify') {\n <!-- OTP \uC778\uC99D \uD6C4 \uC0C8 \uBE44\uBC00\uBC88\uD638 \uC785\uB825 -->\n <form [formGroup]=\"otpForm\" (ngSubmit)=\"verifyOtp()\" class=\"flex flex-col gap-4\">\n <haloduck-input\n data-testid=\"authenticate-otp-code\"\n type=\"text\"\n formControlName=\"code\"\n placeholder=\"Verification Code\"\n />\n <haloduck-input\n data-testid=\"authenticate-otp-password\"\n type=\"password\"\n formControlName=\"newPassword\"\n placeholder=\"New password\"\n />\n <haloduck-button\n data-testid=\"authenticate-otp-submit\"\n type=\"submit\"\n variant=\"primary\"\n (click)=\"verifyOtp()\"\n >Reset Password</haloduck-button\n >\n </form>\n }\n @case ('newPassword') {\n <!-- \uC784\uC2DC \uBE44\uBC00\uBC88\uD638 \u2192 \uC0C8 \uBE44\uBC00\uBC88\uD638 \uBCC0\uACBD -->\n <form\n [formGroup]=\"newPasswordForm\"\n (ngSubmit)=\"confirmNewPassword()\"\n class=\"flex flex-col gap-4\"\n >\n <haloduck-input\n data-testid=\"authenticate-new-password-password\"\n type=\"password\"\n formControlName=\"newPassword\"\n placeholder=\"New password\"\n >\n </haloduck-input>\n <div class=\"text-sm flex flex-col\">\n @for (rule of passwordRules; track rule.label) {\n <span\n [class.text-light-secondary]=\"rule.valid && passwordValue !== ''\"\n [class.line-through]=\"rule.valid && passwordValue !== ''\"\n [class.text-light-danger]=\"!rule.valid && passwordValue !== ''\"\n [class.text-light-inactive]=\"passwordValue === ''\"\n >\n {{ rule.valid ? '\u2714' : '\u2717' }} {{ rule.label }}\n </span>\n }\n </div>\n <haloduck-button\n data-testid=\"authenticate-new-password-submit\"\n type=\"submit\"\n (click)=\"confirmNewPassword()\"\n variant=\"primary\"\n [disabled]=\"!isPasswordValid\"\n >Change Password</haloduck-button\n >\n </form>\n }\n }\n\n <!-- \uB2E8\uACC4 \uC804\uD658 \uD0ED -->\n <div class=\"flex justify-center mt-6 gap-4\">\n @if (stage !== 'login') {\n <haloduck-button\n data-testid=\"authenticate-to-signin\"\n variant=\"none\"\n (click)=\"switchStage('login')\"\n >to Sign In</haloduck-button\n >\n }\n <!-- <haloduck-button class=\"text-sm text-blue-600 hover:underline\"\n (click)=\"switchStage('signup')\">\uD68C\uC6D0\uAC00\uC785</haloduck-button> -->\n @if (stage !== 'reset' && stage !== 'otpVerify' && stage !== 'newPassword') {\n <haloduck-button\n data-testid=\"authenticate-to-reset-password\"\n variant=\"secondary\"\n (click)=\"switchStage('reset')\"\n >Reset Password</haloduck-button\n >\n }\n </div>\n </div>\n</div>\n", styles: [""], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1$1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1$1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "component", type: InputComponent, selector: "haloduck-input", inputs: ["placeholder", "type", "disabled", "rows", "autofocus", "value"] }, { kind: "component", type: ButtonComponent, selector: "haloduck-button", inputs: ["disabled", "variant"] }] });
439
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.1.4", type: AuthenticateComponent, isStandalone: true, selector: "haloduck-authenticate", inputs: { metaData: "metaData", socialLoginProviders: "socialLoginProviders", showSocialLogin: "showSocialLogin", showDivider: "showDivider" }, ngImport: i0, template: "<div class=\"flex flex-col items-center justify-center h-full\">\n <!-- auth.component.html -->\n <div\n class=\"w-full max-w-md mx-auto mt-20 p-8 shadow-lg rounded-2xl bg-light-background dark:bg-dark-background\"\n >\n @switch (stage) {\n @case ('login') {\n <!-- \uC18C\uC15C \uB85C\uADF8\uC778 \uBC84\uD2BC\uB4E4 -->\n @if (showSocialLogin && enabledSocialProviders.length > 0) {\n <div class=\"mb-6\">\n <div class=\"space-y-3\">\n <!-- Google \uB85C\uADF8\uC778 \uBC84\uD2BC -->\n @if (isProviderEnabled('google')) {\n <button\n type=\"button\"\n data-testid=\"authenticate-social-google\"\n (click)=\"loginWithGoogle()\"\n class=\"w-full flex items-center justify-center gap-3 px-4 py-3 border border-gray-300 rounded-lg shadow-sm bg-white hover:bg-gray-50 transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 hover:cursor-pointer\"\n >\n <!-- Google SVG \uC544\uC774\uCF58 -->\n <svg class=\"w-5 h-5\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <path\n 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\"\n fill=\"#4285F4\"\n />\n <path\n 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\"\n fill=\"#34A853\"\n />\n <path\n 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\"\n fill=\"#FBBC05\"\n />\n <path\n 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\"\n fill=\"#EA4335\"\n />\n </svg>\n <span class=\"font-medium text-sm\">{{ getSocialLoginLabel('google') }}</span>\n </button>\n }\n\n <!-- Apple \uB85C\uADF8\uC778 \uBC84\uD2BC -->\n @if (isProviderEnabled('apple')) {\n <button\n type=\"button\"\n data-testid=\"authenticate-social-apple\"\n (click)=\"loginWithApple()\"\n class=\"w-full flex items-center justify-center gap-3 px-4 py-3 border border-black rounded-lg shadow-sm bg-black hover:bg-gray-900 dark:bg-black dark:hover:bg-gray-900 text-white transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 hover:cursor-pointer\"\n >\n <!-- Apple SVG \uC544\uC774\uCF58 -->\n <svg class=\"w-5 h-5 text-white\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <path\n d=\"M17.05 20.28c-.98.95-2.05.8-3.08.35-1.09-.46-2.09-.48-3.24 0-1.44.62-2.2.44-3.06-.35C2.79 15.25 3.51 7.59 9.05 7.31c1.35.07 2.29.74 3.08.8 1.18-.24 2.31-.93 3.57-.84 1.51.12 2.65.72 3.4 1.8-3.12 1.87-2.38 5.98.48 7.13-.57 1.5-1.31 2.99-2.54 4.09l.01-.01zM12.03 7.25c-.15-2.23 1.66-4.07 3.74-4.25.29 2.58-2.34 4.5-3.74 4.25z\"\n />\n </svg>\n <span class=\"font-medium text-sm\">{{ getSocialLoginLabel('apple') }}</span>\n </button>\n }\n\n <!-- Kakao \uB85C\uADF8\uC778 \uBC84\uD2BC -->\n @if (isProviderEnabled('kakao')) {\n <button\n type=\"button\"\n data-testid=\"authenticate-social-kakao\"\n (click)=\"loginWithKakao()\"\n class=\"w-full flex items-center justify-center gap-3 px-4 py-3 border border-yellow-400 rounded-lg shadow-sm bg-yellow-400 hover:bg-yellow-500 text-black transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 hover:cursor-pointer\"\n >\n <!-- Kakao SVG \uC544\uC774\uCF58 -->\n <svg class=\"w-5 h-5 text-black\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <path\n d=\"M12 3C7.03 3 3 6.17 3 10.08c0 2.53 1.67 4.74 4.19 6.08L6.45 18.9c-.14.22-.02.52.24.52.08 0 .17-.03.23-.09L9.2 17.4c.87.17 1.8.26 2.8.26s1.93-.09 2.8-.26l2.28 1.93c.06.06.15.09.23.09.26 0 .38-.3.24-.52l-.74-2.74C19.33 14.82 21 12.61 21 10.08 21 6.17 16.97 3 12 3z\"\n />\n </svg>\n <span class=\"font-medium text-sm\">{{ getSocialLoginLabel('kakao') }}</span>\n </button>\n }\n </div>\n\n <!-- \uAD6C\uBD84\uC120 -->\n @if (showDivider) {\n <div class=\"relative my-6\">\n <div class=\"absolute inset-0 flex items-center\">\n <div class=\"w-full border-t border-gray-300 dark:border-gray-600\"></div>\n </div>\n <div class=\"relative flex justify-center text-sm\">\n <span\n class=\"px-2 bg-light-background dark:bg-dark-background text-gray-500 dark:text-gray-400\"\n >or</span\n >\n </div>\n </div>\n }\n </div>\n }\n\n <!-- \uB85C\uADF8\uC778 -->\n <form [formGroup]=\"loginForm\" (ngSubmit)=\"login()\" class=\"flex flex-col gap-4\">\n <haloduck-input\n data-testid=\"authenticate-login-email\"\n type=\"email\"\n formControlName=\"email\"\n placeholder=\"Email\"\n />\n <haloduck-input\n data-testid=\"authenticate-login-password\"\n type=\"password\"\n formControlName=\"password\"\n placeholder=\"Password\"\n />\n <haloduck-button\n data-testid=\"authenticate-login-submit\"\n type=\"submit\"\n variant=\"primary\"\n (click)=\"login()\"\n >Sign In</haloduck-button\n >\n </form>\n }\n @case ('signup') {\n <!-- \uC18C\uC15C \uB85C\uADF8\uC778 \uBC84\uD2BC\uB4E4 (\uD68C\uC6D0\uAC00\uC785) -->\n @if (showSocialLogin && enabledSocialProviders.length > 0) {\n <div class=\"mb-6\">\n <div class=\"space-y-3\">\n <!-- Google \uB85C\uADF8\uC778 \uBC84\uD2BC -->\n @if (isProviderEnabled('google')) {\n <button\n type=\"button\"\n data-testid=\"authenticate-signup-social-google\"\n (click)=\"loginWithGoogle()\"\n class=\"w-full flex items-center justify-center gap-3 px-4 py-3 border border-gray-300 rounded-lg shadow-sm bg-white hover:bg-gray-50 transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 hover:cursor-pointer\"\n >\n <!-- Google SVG \uC544\uC774\uCF58 -->\n <svg class=\"w-5 h-5\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <path\n 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\"\n fill=\"#4285F4\"\n />\n <path\n 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\"\n fill=\"#34A853\"\n />\n <path\n 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\"\n fill=\"#FBBC05\"\n />\n <path\n 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\"\n fill=\"#EA4335\"\n />\n </svg>\n <span class=\"font-medium text-sm\">{{ getSocialLoginLabel('google') }}</span>\n </button>\n }\n\n <!-- Apple \uB85C\uADF8\uC778 \uBC84\uD2BC -->\n @if (isProviderEnabled('apple')) {\n <button\n type=\"button\"\n data-testid=\"authenticate-signup-social-apple\"\n (click)=\"loginWithApple()\"\n class=\"w-full flex items-center justify-center gap-3 px-4 py-3 border border-black rounded-lg shadow-sm bg-black hover:bg-gray-900 dark:bg-black dark:hover:bg-gray-900 text-white transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 hover:cursor-pointer\"\n >\n <!-- Apple SVG \uC544\uC774\uCF58 -->\n <svg class=\"w-5 h-5 text-white\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <path\n d=\"M17.05 20.28c-.98.95-2.05.8-3.08.35-1.09-.46-2.09-.48-3.24 0-1.44.62-2.2.44-3.06-.35C2.79 15.25 3.51 7.59 9.05 7.31c1.35.07 2.29.74 3.08.8 1.18-.24 2.31-.93 3.57-.84 1.51.12 2.65.72 3.4 1.8-3.12 1.87-2.38 5.98.48 7.13-.57 1.5-1.31 2.99-2.54 4.09l.01-.01zM12.03 7.25c-.15-2.23 1.66-4.07 3.74-4.25.29 2.58-2.34 4.5-3.74 4.25z\"\n />\n </svg>\n <span class=\"font-medium text-sm\">{{ getSocialLoginLabel('apple') }}</span>\n </button>\n }\n\n <!-- Kakao \uB85C\uADF8\uC778 \uBC84\uD2BC -->\n @if (isProviderEnabled('kakao')) {\n <button\n type=\"button\"\n data-testid=\"authenticate-signup-social-kakao\"\n (click)=\"loginWithKakao()\"\n class=\"w-full flex items-center justify-center gap-3 px-4 py-3 border border-yellow-400 rounded-lg shadow-sm bg-yellow-400 hover:bg-yellow-500 text-black transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 hover:cursor-pointer\"\n >\n <!-- Kakao SVG \uC544\uC774\uCF58 -->\n <svg class=\"w-5 h-5 text-black\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <path\n d=\"M12 3C7.03 3 3 6.17 3 10.08c0 2.53 1.67 4.74 4.19 6.08L6.45 18.9c-.14.22-.02.52.24.52.08 0 .17-.03.23-.09L9.2 17.4c.87.17 1.8.26 2.8.26s1.93-.09 2.8-.26l2.28 1.93c.06.06.15.09.23.09.26 0 .38-.3.24-.52l-.74-2.74C19.33 14.82 21 12.61 21 10.08 21 6.17 16.97 3 12 3z\"\n />\n </svg>\n <span class=\"font-medium text-sm\">{{ getSocialLoginLabel('kakao') }}</span>\n </button>\n }\n </div>\n\n <!-- \uAD6C\uBD84\uC120 -->\n @if (showDivider) {\n <div class=\"relative my-6\">\n <div class=\"absolute inset-0 flex items-center\">\n <div class=\"w-full border-t border-gray-300 dark:border-gray-600\"></div>\n </div>\n <div class=\"relative flex justify-center text-sm\">\n <span\n class=\"px-2 bg-light-background dark:bg-dark-background text-gray-500 dark:text-gray-400\"\n >or</span\n >\n </div>\n </div>\n }\n </div>\n }\n\n <!-- \uD68C\uC6D0\uAC00\uC785 -->\n <form [formGroup]=\"signupForm\" (ngSubmit)=\"signup()\" class=\"flex flex-col gap-4\">\n <haloduck-input\n data-testid=\"authenticate-signup-email\"\n type=\"email\"\n formControlName=\"email\"\n placeholder=\"Email\"\n />\n <haloduck-input\n data-testid=\"authenticate-signup-password\"\n type=\"password\"\n formControlName=\"password\"\n placeholder=\"Password\"\n />\n <haloduck-button data-testid=\"authenticate-signup-submit\" type=\"submit\" variant=\"primary\"\n >Sign Up</haloduck-button\n >\n </form>\n }\n @case ('reset') {\n <!-- \uBE44\uBC00\uBC88\uD638 \uC7AC\uC124\uC815 \uC694\uCCAD -->\n <form [formGroup]=\"resetForm\" (ngSubmit)=\"reset()\" class=\"flex flex-col gap-4\">\n <haloduck-input\n data-testid=\"authenticate-reset-email\"\n type=\"email\"\n formControlName=\"email\"\n placeholder=\"Email\"\n />\n <haloduck-button\n data-testid=\"authenticate-reset-submit\"\n type=\"submit\"\n variant=\"primary\"\n (click)=\"reset()\"\n >Send Verification Code</haloduck-button\n >\n </form>\n }\n @case ('otpVerify') {\n <!-- OTP \uC778\uC99D \uD6C4 \uC0C8 \uBE44\uBC00\uBC88\uD638 \uC785\uB825 -->\n <form [formGroup]=\"otpForm\" (ngSubmit)=\"verifyOtp()\" class=\"flex flex-col gap-4\">\n <haloduck-input\n data-testid=\"authenticate-otp-code\"\n type=\"text\"\n formControlName=\"code\"\n placeholder=\"Verification Code\"\n />\n <haloduck-input\n data-testid=\"authenticate-otp-password\"\n type=\"password\"\n formControlName=\"newPassword\"\n placeholder=\"New password\"\n />\n <haloduck-button\n data-testid=\"authenticate-otp-submit\"\n type=\"submit\"\n variant=\"primary\"\n (click)=\"verifyOtp()\"\n >Reset Password</haloduck-button\n >\n </form>\n }\n @case ('newPassword') {\n <!-- \uC784\uC2DC \uBE44\uBC00\uBC88\uD638 \u2192 \uC0C8 \uBE44\uBC00\uBC88\uD638 \uBCC0\uACBD -->\n <form\n [formGroup]=\"newPasswordForm\"\n (ngSubmit)=\"confirmNewPassword()\"\n class=\"flex flex-col gap-4\"\n >\n <haloduck-input\n data-testid=\"authenticate-new-password-password\"\n type=\"password\"\n formControlName=\"newPassword\"\n placeholder=\"New password\"\n >\n </haloduck-input>\n <div class=\"text-sm flex flex-col\">\n @for (rule of passwordRules; track rule.label) {\n <span\n [class.text-light-secondary]=\"rule.valid && passwordValue !== ''\"\n [class.line-through]=\"rule.valid && passwordValue !== ''\"\n [class.text-light-danger]=\"!rule.valid && passwordValue !== ''\"\n [class.text-light-inactive]=\"passwordValue === ''\"\n >\n {{ rule.valid ? '\u2714' : '\u2717' }} {{ rule.label }}\n </span>\n }\n </div>\n <haloduck-button\n data-testid=\"authenticate-new-password-submit\"\n type=\"submit\"\n (click)=\"confirmNewPassword()\"\n variant=\"primary\"\n [disabled]=\"!isPasswordValid\"\n >Change Password</haloduck-button\n >\n </form>\n }\n }\n\n <!-- \uB2E8\uACC4 \uC804\uD658 \uD0ED -->\n <div class=\"flex flex-col items-center mt-6 gap-4\">\n <div class=\"flex justify-center gap-4\">\n @if (stage !== 'login') {\n <haloduck-button\n data-testid=\"authenticate-to-signin\"\n variant=\"none\"\n (click)=\"switchStage('login')\"\n >to Sign In</haloduck-button\n >\n }\n @if (stage !== 'signup') {\n <haloduck-button\n data-testid=\"authenticate-to-signup\"\n variant=\"none\"\n (click)=\"switchStage('signup')\"\n >to Sign Up</haloduck-button\n >\n }\n @if (stage !== 'reset' && stage !== 'otpVerify' && stage !== 'newPassword') {\n <haloduck-button\n data-testid=\"authenticate-to-reset-password\"\n variant=\"secondary\"\n (click)=\"switchStage('reset')\"\n >Reset Password</haloduck-button\n >\n }\n </div>\n </div>\n </div>\n</div>\n", styles: [""], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1$1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1$1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "component", type: InputComponent, selector: "haloduck-input", inputs: ["placeholder", "type", "disabled", "rows", "autofocus", "value"] }, { kind: "component", type: ButtonComponent, selector: "haloduck-button", inputs: ["disabled", "variant"] }, { kind: "ngmodule", type: CommonModule }] });
342
440
  }
343
441
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: AuthenticateComponent, decorators: [{
344
442
  type: Component,
345
- args: [{ selector: 'haloduck-authenticate', imports: [FormsModule, ReactiveFormsModule, InputComponent, ButtonComponent], template: "<div class=\"flex flex-col items-center justify-center h-full\">\n <!-- auth.component.html -->\n <div\n class=\"w-full max-w-md mx-auto mt-20 p-8 shadow-lg rounded-2xl bg-light-background dark:bg-dark-background\"\n >\n @switch (stage) {\n @case ('login') {\n <!-- \uB85C\uADF8\uC778 -->\n <form [formGroup]=\"loginForm\" (ngSubmit)=\"login()\" class=\"flex flex-col gap-4\">\n <haloduck-input\n data-testid=\"authenticate-login-email\"\n type=\"email\"\n formControlName=\"email\"\n placeholder=\"Email\"\n />\n <haloduck-input\n data-testid=\"authenticate-login-password\"\n type=\"password\"\n formControlName=\"password\"\n placeholder=\"Password\"\n />\n <haloduck-button\n data-testid=\"authenticate-login-submit\"\n type=\"submit\"\n variant=\"primary\"\n (click)=\"login()\"\n >Sign In</haloduck-button\n >\n </form>\n }\n @case ('signup') {\n <!-- \uD68C\uC6D0\uAC00\uC785 -->\n <form [formGroup]=\"signupForm\" (ngSubmit)=\"signup()\" class=\"flex flex-col gap-4\">\n <haloduck-input\n data-testid=\"authenticate-signup-email\"\n type=\"email\"\n formControlName=\"email\"\n placeholder=\"Email\"\n />\n <haloduck-input\n data-testid=\"authenticate-signup-password\"\n type=\"password\"\n formControlName=\"password\"\n placeholder=\"Password\"\n />\n <haloduck-button data-testid=\"authenticate-signup-submit\" type=\"submit\" variant=\"primary\"\n >Sign Up</haloduck-button\n >\n </form>\n }\n @case ('reset') {\n <!-- \uBE44\uBC00\uBC88\uD638 \uC7AC\uC124\uC815 \uC694\uCCAD -->\n <form [formGroup]=\"resetForm\" (ngSubmit)=\"reset()\" class=\"flex flex-col gap-4\">\n <haloduck-input\n data-testid=\"authenticate-reset-email\"\n type=\"email\"\n formControlName=\"email\"\n placeholder=\"Email\"\n />\n <haloduck-button\n data-testid=\"authenticate-reset-submit\"\n type=\"submit\"\n variant=\"primary\"\n (click)=\"reset()\"\n >Send Verification Code</haloduck-button\n >\n </form>\n }\n @case ('otpVerify') {\n <!-- OTP \uC778\uC99D \uD6C4 \uC0C8 \uBE44\uBC00\uBC88\uD638 \uC785\uB825 -->\n <form [formGroup]=\"otpForm\" (ngSubmit)=\"verifyOtp()\" class=\"flex flex-col gap-4\">\n <haloduck-input\n data-testid=\"authenticate-otp-code\"\n type=\"text\"\n formControlName=\"code\"\n placeholder=\"Verification Code\"\n />\n <haloduck-input\n data-testid=\"authenticate-otp-password\"\n type=\"password\"\n formControlName=\"newPassword\"\n placeholder=\"New password\"\n />\n <haloduck-button\n data-testid=\"authenticate-otp-submit\"\n type=\"submit\"\n variant=\"primary\"\n (click)=\"verifyOtp()\"\n >Reset Password</haloduck-button\n >\n </form>\n }\n @case ('newPassword') {\n <!-- \uC784\uC2DC \uBE44\uBC00\uBC88\uD638 \u2192 \uC0C8 \uBE44\uBC00\uBC88\uD638 \uBCC0\uACBD -->\n <form\n [formGroup]=\"newPasswordForm\"\n (ngSubmit)=\"confirmNewPassword()\"\n class=\"flex flex-col gap-4\"\n >\n <haloduck-input\n data-testid=\"authenticate-new-password-password\"\n type=\"password\"\n formControlName=\"newPassword\"\n placeholder=\"New password\"\n >\n </haloduck-input>\n <div class=\"text-sm flex flex-col\">\n @for (rule of passwordRules; track rule.label) {\n <span\n [class.text-light-secondary]=\"rule.valid && passwordValue !== ''\"\n [class.line-through]=\"rule.valid && passwordValue !== ''\"\n [class.text-light-danger]=\"!rule.valid && passwordValue !== ''\"\n [class.text-light-inactive]=\"passwordValue === ''\"\n >\n {{ rule.valid ? '\u2714' : '\u2717' }} {{ rule.label }}\n </span>\n }\n </div>\n <haloduck-button\n data-testid=\"authenticate-new-password-submit\"\n type=\"submit\"\n (click)=\"confirmNewPassword()\"\n variant=\"primary\"\n [disabled]=\"!isPasswordValid\"\n >Change Password</haloduck-button\n >\n </form>\n }\n }\n\n <!-- \uB2E8\uACC4 \uC804\uD658 \uD0ED -->\n <div class=\"flex justify-center mt-6 gap-4\">\n @if (stage !== 'login') {\n <haloduck-button\n data-testid=\"authenticate-to-signin\"\n variant=\"none\"\n (click)=\"switchStage('login')\"\n >to Sign In</haloduck-button\n >\n }\n <!-- <haloduck-button class=\"text-sm text-blue-600 hover:underline\"\n (click)=\"switchStage('signup')\">\uD68C\uC6D0\uAC00\uC785</haloduck-button> -->\n @if (stage !== 'reset' && stage !== 'otpVerify' && stage !== 'newPassword') {\n <haloduck-button\n data-testid=\"authenticate-to-reset-password\"\n variant=\"secondary\"\n (click)=\"switchStage('reset')\"\n >Reset Password</haloduck-button\n >\n }\n </div>\n </div>\n</div>\n" }]
346
- }], ctorParameters: () => [{ type: i1$1.FormBuilder }, { type: i2.HttpClient }] });
443
+ args: [{ selector: 'haloduck-authenticate', imports: [FormsModule, ReactiveFormsModule, InputComponent, ButtonComponent, CommonModule], template: "<div class=\"flex flex-col items-center justify-center h-full\">\n <!-- auth.component.html -->\n <div\n class=\"w-full max-w-md mx-auto mt-20 p-8 shadow-lg rounded-2xl bg-light-background dark:bg-dark-background\"\n >\n @switch (stage) {\n @case ('login') {\n <!-- \uC18C\uC15C \uB85C\uADF8\uC778 \uBC84\uD2BC\uB4E4 -->\n @if (showSocialLogin && enabledSocialProviders.length > 0) {\n <div class=\"mb-6\">\n <div class=\"space-y-3\">\n <!-- Google \uB85C\uADF8\uC778 \uBC84\uD2BC -->\n @if (isProviderEnabled('google')) {\n <button\n type=\"button\"\n data-testid=\"authenticate-social-google\"\n (click)=\"loginWithGoogle()\"\n class=\"w-full flex items-center justify-center gap-3 px-4 py-3 border border-gray-300 rounded-lg shadow-sm bg-white hover:bg-gray-50 transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 hover:cursor-pointer\"\n >\n <!-- Google SVG \uC544\uC774\uCF58 -->\n <svg class=\"w-5 h-5\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <path\n 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\"\n fill=\"#4285F4\"\n />\n <path\n 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\"\n fill=\"#34A853\"\n />\n <path\n 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\"\n fill=\"#FBBC05\"\n />\n <path\n 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\"\n fill=\"#EA4335\"\n />\n </svg>\n <span class=\"font-medium text-sm\">{{ getSocialLoginLabel('google') }}</span>\n </button>\n }\n\n <!-- Apple \uB85C\uADF8\uC778 \uBC84\uD2BC -->\n @if (isProviderEnabled('apple')) {\n <button\n type=\"button\"\n data-testid=\"authenticate-social-apple\"\n (click)=\"loginWithApple()\"\n class=\"w-full flex items-center justify-center gap-3 px-4 py-3 border border-black rounded-lg shadow-sm bg-black hover:bg-gray-900 dark:bg-black dark:hover:bg-gray-900 text-white transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 hover:cursor-pointer\"\n >\n <!-- Apple SVG \uC544\uC774\uCF58 -->\n <svg class=\"w-5 h-5 text-white\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <path\n d=\"M17.05 20.28c-.98.95-2.05.8-3.08.35-1.09-.46-2.09-.48-3.24 0-1.44.62-2.2.44-3.06-.35C2.79 15.25 3.51 7.59 9.05 7.31c1.35.07 2.29.74 3.08.8 1.18-.24 2.31-.93 3.57-.84 1.51.12 2.65.72 3.4 1.8-3.12 1.87-2.38 5.98.48 7.13-.57 1.5-1.31 2.99-2.54 4.09l.01-.01zM12.03 7.25c-.15-2.23 1.66-4.07 3.74-4.25.29 2.58-2.34 4.5-3.74 4.25z\"\n />\n </svg>\n <span class=\"font-medium text-sm\">{{ getSocialLoginLabel('apple') }}</span>\n </button>\n }\n\n <!-- Kakao \uB85C\uADF8\uC778 \uBC84\uD2BC -->\n @if (isProviderEnabled('kakao')) {\n <button\n type=\"button\"\n data-testid=\"authenticate-social-kakao\"\n (click)=\"loginWithKakao()\"\n class=\"w-full flex items-center justify-center gap-3 px-4 py-3 border border-yellow-400 rounded-lg shadow-sm bg-yellow-400 hover:bg-yellow-500 text-black transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 hover:cursor-pointer\"\n >\n <!-- Kakao SVG \uC544\uC774\uCF58 -->\n <svg class=\"w-5 h-5 text-black\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <path\n d=\"M12 3C7.03 3 3 6.17 3 10.08c0 2.53 1.67 4.74 4.19 6.08L6.45 18.9c-.14.22-.02.52.24.52.08 0 .17-.03.23-.09L9.2 17.4c.87.17 1.8.26 2.8.26s1.93-.09 2.8-.26l2.28 1.93c.06.06.15.09.23.09.26 0 .38-.3.24-.52l-.74-2.74C19.33 14.82 21 12.61 21 10.08 21 6.17 16.97 3 12 3z\"\n />\n </svg>\n <span class=\"font-medium text-sm\">{{ getSocialLoginLabel('kakao') }}</span>\n </button>\n }\n </div>\n\n <!-- \uAD6C\uBD84\uC120 -->\n @if (showDivider) {\n <div class=\"relative my-6\">\n <div class=\"absolute inset-0 flex items-center\">\n <div class=\"w-full border-t border-gray-300 dark:border-gray-600\"></div>\n </div>\n <div class=\"relative flex justify-center text-sm\">\n <span\n class=\"px-2 bg-light-background dark:bg-dark-background text-gray-500 dark:text-gray-400\"\n >or</span\n >\n </div>\n </div>\n }\n </div>\n }\n\n <!-- \uB85C\uADF8\uC778 -->\n <form [formGroup]=\"loginForm\" (ngSubmit)=\"login()\" class=\"flex flex-col gap-4\">\n <haloduck-input\n data-testid=\"authenticate-login-email\"\n type=\"email\"\n formControlName=\"email\"\n placeholder=\"Email\"\n />\n <haloduck-input\n data-testid=\"authenticate-login-password\"\n type=\"password\"\n formControlName=\"password\"\n placeholder=\"Password\"\n />\n <haloduck-button\n data-testid=\"authenticate-login-submit\"\n type=\"submit\"\n variant=\"primary\"\n (click)=\"login()\"\n >Sign In</haloduck-button\n >\n </form>\n }\n @case ('signup') {\n <!-- \uC18C\uC15C \uB85C\uADF8\uC778 \uBC84\uD2BC\uB4E4 (\uD68C\uC6D0\uAC00\uC785) -->\n @if (showSocialLogin && enabledSocialProviders.length > 0) {\n <div class=\"mb-6\">\n <div class=\"space-y-3\">\n <!-- Google \uB85C\uADF8\uC778 \uBC84\uD2BC -->\n @if (isProviderEnabled('google')) {\n <button\n type=\"button\"\n data-testid=\"authenticate-signup-social-google\"\n (click)=\"loginWithGoogle()\"\n class=\"w-full flex items-center justify-center gap-3 px-4 py-3 border border-gray-300 rounded-lg shadow-sm bg-white hover:bg-gray-50 transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 hover:cursor-pointer\"\n >\n <!-- Google SVG \uC544\uC774\uCF58 -->\n <svg class=\"w-5 h-5\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <path\n 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\"\n fill=\"#4285F4\"\n />\n <path\n 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\"\n fill=\"#34A853\"\n />\n <path\n 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\"\n fill=\"#FBBC05\"\n />\n <path\n 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\"\n fill=\"#EA4335\"\n />\n </svg>\n <span class=\"font-medium text-sm\">{{ getSocialLoginLabel('google') }}</span>\n </button>\n }\n\n <!-- Apple \uB85C\uADF8\uC778 \uBC84\uD2BC -->\n @if (isProviderEnabled('apple')) {\n <button\n type=\"button\"\n data-testid=\"authenticate-signup-social-apple\"\n (click)=\"loginWithApple()\"\n class=\"w-full flex items-center justify-center gap-3 px-4 py-3 border border-black rounded-lg shadow-sm bg-black hover:bg-gray-900 dark:bg-black dark:hover:bg-gray-900 text-white transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 hover:cursor-pointer\"\n >\n <!-- Apple SVG \uC544\uC774\uCF58 -->\n <svg class=\"w-5 h-5 text-white\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <path\n d=\"M17.05 20.28c-.98.95-2.05.8-3.08.35-1.09-.46-2.09-.48-3.24 0-1.44.62-2.2.44-3.06-.35C2.79 15.25 3.51 7.59 9.05 7.31c1.35.07 2.29.74 3.08.8 1.18-.24 2.31-.93 3.57-.84 1.51.12 2.65.72 3.4 1.8-3.12 1.87-2.38 5.98.48 7.13-.57 1.5-1.31 2.99-2.54 4.09l.01-.01zM12.03 7.25c-.15-2.23 1.66-4.07 3.74-4.25.29 2.58-2.34 4.5-3.74 4.25z\"\n />\n </svg>\n <span class=\"font-medium text-sm\">{{ getSocialLoginLabel('apple') }}</span>\n </button>\n }\n\n <!-- Kakao \uB85C\uADF8\uC778 \uBC84\uD2BC -->\n @if (isProviderEnabled('kakao')) {\n <button\n type=\"button\"\n data-testid=\"authenticate-signup-social-kakao\"\n (click)=\"loginWithKakao()\"\n class=\"w-full flex items-center justify-center gap-3 px-4 py-3 border border-yellow-400 rounded-lg shadow-sm bg-yellow-400 hover:bg-yellow-500 text-black transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 hover:cursor-pointer\"\n >\n <!-- Kakao SVG \uC544\uC774\uCF58 -->\n <svg class=\"w-5 h-5 text-black\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <path\n d=\"M12 3C7.03 3 3 6.17 3 10.08c0 2.53 1.67 4.74 4.19 6.08L6.45 18.9c-.14.22-.02.52.24.52.08 0 .17-.03.23-.09L9.2 17.4c.87.17 1.8.26 2.8.26s1.93-.09 2.8-.26l2.28 1.93c.06.06.15.09.23.09.26 0 .38-.3.24-.52l-.74-2.74C19.33 14.82 21 12.61 21 10.08 21 6.17 16.97 3 12 3z\"\n />\n </svg>\n <span class=\"font-medium text-sm\">{{ getSocialLoginLabel('kakao') }}</span>\n </button>\n }\n </div>\n\n <!-- \uAD6C\uBD84\uC120 -->\n @if (showDivider) {\n <div class=\"relative my-6\">\n <div class=\"absolute inset-0 flex items-center\">\n <div class=\"w-full border-t border-gray-300 dark:border-gray-600\"></div>\n </div>\n <div class=\"relative flex justify-center text-sm\">\n <span\n class=\"px-2 bg-light-background dark:bg-dark-background text-gray-500 dark:text-gray-400\"\n >or</span\n >\n </div>\n </div>\n }\n </div>\n }\n\n <!-- \uD68C\uC6D0\uAC00\uC785 -->\n <form [formGroup]=\"signupForm\" (ngSubmit)=\"signup()\" class=\"flex flex-col gap-4\">\n <haloduck-input\n data-testid=\"authenticate-signup-email\"\n type=\"email\"\n formControlName=\"email\"\n placeholder=\"Email\"\n />\n <haloduck-input\n data-testid=\"authenticate-signup-password\"\n type=\"password\"\n formControlName=\"password\"\n placeholder=\"Password\"\n />\n <haloduck-button data-testid=\"authenticate-signup-submit\" type=\"submit\" variant=\"primary\"\n >Sign Up</haloduck-button\n >\n </form>\n }\n @case ('reset') {\n <!-- \uBE44\uBC00\uBC88\uD638 \uC7AC\uC124\uC815 \uC694\uCCAD -->\n <form [formGroup]=\"resetForm\" (ngSubmit)=\"reset()\" class=\"flex flex-col gap-4\">\n <haloduck-input\n data-testid=\"authenticate-reset-email\"\n type=\"email\"\n formControlName=\"email\"\n placeholder=\"Email\"\n />\n <haloduck-button\n data-testid=\"authenticate-reset-submit\"\n type=\"submit\"\n variant=\"primary\"\n (click)=\"reset()\"\n >Send Verification Code</haloduck-button\n >\n </form>\n }\n @case ('otpVerify') {\n <!-- OTP \uC778\uC99D \uD6C4 \uC0C8 \uBE44\uBC00\uBC88\uD638 \uC785\uB825 -->\n <form [formGroup]=\"otpForm\" (ngSubmit)=\"verifyOtp()\" class=\"flex flex-col gap-4\">\n <haloduck-input\n data-testid=\"authenticate-otp-code\"\n type=\"text\"\n formControlName=\"code\"\n placeholder=\"Verification Code\"\n />\n <haloduck-input\n data-testid=\"authenticate-otp-password\"\n type=\"password\"\n formControlName=\"newPassword\"\n placeholder=\"New password\"\n />\n <haloduck-button\n data-testid=\"authenticate-otp-submit\"\n type=\"submit\"\n variant=\"primary\"\n (click)=\"verifyOtp()\"\n >Reset Password</haloduck-button\n >\n </form>\n }\n @case ('newPassword') {\n <!-- \uC784\uC2DC \uBE44\uBC00\uBC88\uD638 \u2192 \uC0C8 \uBE44\uBC00\uBC88\uD638 \uBCC0\uACBD -->\n <form\n [formGroup]=\"newPasswordForm\"\n (ngSubmit)=\"confirmNewPassword()\"\n class=\"flex flex-col gap-4\"\n >\n <haloduck-input\n data-testid=\"authenticate-new-password-password\"\n type=\"password\"\n formControlName=\"newPassword\"\n placeholder=\"New password\"\n >\n </haloduck-input>\n <div class=\"text-sm flex flex-col\">\n @for (rule of passwordRules; track rule.label) {\n <span\n [class.text-light-secondary]=\"rule.valid && passwordValue !== ''\"\n [class.line-through]=\"rule.valid && passwordValue !== ''\"\n [class.text-light-danger]=\"!rule.valid && passwordValue !== ''\"\n [class.text-light-inactive]=\"passwordValue === ''\"\n >\n {{ rule.valid ? '\u2714' : '\u2717' }} {{ rule.label }}\n </span>\n }\n </div>\n <haloduck-button\n data-testid=\"authenticate-new-password-submit\"\n type=\"submit\"\n (click)=\"confirmNewPassword()\"\n variant=\"primary\"\n [disabled]=\"!isPasswordValid\"\n >Change Password</haloduck-button\n >\n </form>\n }\n }\n\n <!-- \uB2E8\uACC4 \uC804\uD658 \uD0ED -->\n <div class=\"flex flex-col items-center mt-6 gap-4\">\n <div class=\"flex justify-center gap-4\">\n @if (stage !== 'login') {\n <haloduck-button\n data-testid=\"authenticate-to-signin\"\n variant=\"none\"\n (click)=\"switchStage('login')\"\n >to Sign In</haloduck-button\n >\n }\n @if (stage !== 'signup') {\n <haloduck-button\n data-testid=\"authenticate-to-signup\"\n variant=\"none\"\n (click)=\"switchStage('signup')\"\n >to Sign Up</haloduck-button\n >\n }\n @if (stage !== 'reset' && stage !== 'otpVerify' && stage !== 'newPassword') {\n <haloduck-button\n data-testid=\"authenticate-to-reset-password\"\n variant=\"secondary\"\n (click)=\"switchStage('reset')\"\n >Reset Password</haloduck-button\n >\n }\n </div>\n </div>\n </div>\n</div>\n" }]
444
+ }], ctorParameters: () => [{ type: i1$1.FormBuilder }, { type: i2.HttpClient }], propDecorators: { metaData: [{
445
+ type: Input
446
+ }], socialLoginProviders: [{
447
+ type: Input
448
+ }], showSocialLogin: [{
449
+ type: Input
450
+ }], showDivider: [{
451
+ type: Input
452
+ }] } });
453
+
454
+ class AuthenticateCallbackComponent {
455
+ route = inject(ActivatedRoute);
456
+ router = inject(Router);
457
+ notificationService = inject(NotificationService);
458
+ isProcessing = true;
459
+ error = null;
460
+ success = false;
461
+ ngOnInit() {
462
+ this.handleSocialCallback();
463
+ }
464
+ async handleSocialCallback() {
465
+ try {
466
+ // URL의 쿼리 파라미터에서 data 추출
467
+ const queryParams = this.route.snapshot.queryParams;
468
+ const encodedData = queryParams['data'];
469
+ if (!encodedData) {
470
+ throw new Error('ui.authenticate.Social login data not found.');
471
+ }
472
+ // Base64 디코딩
473
+ const decodedData = this.decodeBase64(encodedData);
474
+ const cognitoUserResponse = JSON.parse(decodedData);
475
+ // Amplify 로그인 처리
476
+ await this.processAmplifyLogin(cognitoUserResponse);
477
+ this.success = true;
478
+ this.isProcessing = false;
479
+ this.notificationService.showNotification('success', '소셜 로그인이 완료되었습니다.', 3000);
480
+ // 로그인 성공 후 리다이렉트
481
+ setTimeout(() => {
482
+ this.redirectAfterLogin();
483
+ }, 500);
484
+ }
485
+ catch (error) {
486
+ console.error('Social callback error:', error);
487
+ this.error = error instanceof Error ? error.message : '로그인 처리 중 오류가 발생했습니다.';
488
+ this.isProcessing = false;
489
+ this.notificationService.showNotification('error', this.error);
490
+ }
491
+ }
492
+ decodeBase64(encodedString) {
493
+ try {
494
+ // URL-safe base64 디코딩을 위해 문자 치환
495
+ const base64String = encodedString.replace(/-/g, '+').replace(/_/g, '/');
496
+ // 패딩 추가 (필요한 경우)
497
+ const padding = base64String.length % 4;
498
+ const paddedString = padding ? base64String + '='.repeat(4 - padding) : base64String;
499
+ return atob(paddedString);
500
+ }
501
+ catch (error) {
502
+ throw new Error('ui.authenticate.Invalid data format.');
503
+ }
504
+ }
505
+ async processAmplifyLogin(cognitoUserResponse) {
506
+ try {
507
+ console.log('Processing Amplify login with:', {
508
+ email: cognitoUserResponse.email,
509
+ hasAuthResult: !!cognitoUserResponse.authResult,
510
+ });
511
+ if (cognitoUserResponse.authResult) {
512
+ const { AccessToken, IdToken, RefreshToken } = cognitoUserResponse.authResult;
513
+ // Cognito 토큰을 로컬스토리지에 저장 (Amplify가 사용할 수 있는 형태로)
514
+ const keyPrefix = `CognitoIdentityServiceProvider.${await this.getClientId()}`;
515
+ const username = cognitoUserResponse.email;
516
+ // Amplify가 인식할 수 있는 형태로 토큰들을 저장
517
+ localStorage.setItem(`${keyPrefix}.${username}.accessToken`, AccessToken);
518
+ localStorage.setItem(`${keyPrefix}.${username}.idToken`, IdToken);
519
+ localStorage.setItem(`${keyPrefix}.${username}.refreshToken`, RefreshToken);
520
+ localStorage.setItem(`${keyPrefix}.LastAuthUser`, username);
521
+ localStorage.setItem(`${keyPrefix}.${username}.tokenScopesAccepted`, '1');
522
+ // Amplify Hub를 통해 인증 상태 변경 알림
523
+ Hub.dispatch('auth', {
524
+ event: 'signedIn',
525
+ data: {
526
+ username: cognitoUserResponse.email,
527
+ signInDetails: {
528
+ loginId: cognitoUserResponse.email,
529
+ authFlowType: 'CUSTOM_AUTH',
530
+ },
531
+ },
532
+ });
533
+ // 세션 새로고침을 통해 Amplify가 토큰을 인식하도록 함
534
+ try {
535
+ await fetchAuthSession({ forceRefresh: true });
536
+ }
537
+ catch (sessionError) {
538
+ console.warn('Session refresh failed, but tokens are stored:', sessionError);
539
+ }
540
+ }
541
+ }
542
+ catch (error) {
543
+ console.error('Amplify login processing error:', error);
544
+ throw new Error('ui.authenticate.Authentication processing failed.');
545
+ }
546
+ }
547
+ async getClientId() {
548
+ // Amplify 설정에서 클라이언트 ID를 가져오는 로직
549
+ // 실제 환경에서는 Amplify.configure에서 설정된 값을 사용
550
+ try {
551
+ const { Amplify } = await import('aws-amplify');
552
+ const config = Amplify.getConfig();
553
+ return config.Auth?.Cognito?.userPoolClientId || 'default-client-id';
554
+ }
555
+ catch {
556
+ // fallback - 환경변수나 설정파일에서 가져올 수 있습니다
557
+ return 'default-client-id';
558
+ }
559
+ }
560
+ redirectToLogin() {
561
+ // 로그인 페이지로 리다이렉트 (라우트는 실제 애플리케이션에 맞게 조정)
562
+ this.router.navigate(['/auth/login']);
563
+ }
564
+ redirectAfterLogin() {
565
+ // 로그인 성공 후 리다이렉트할 페이지
566
+ // returnUrl이 있으면 그곳으로, 없으면 기본 페이지로
567
+ const returnUrl = this.route.snapshot.queryParams['returnUrl'] || '/';
568
+ this.router.navigate([returnUrl]);
569
+ }
570
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: AuthenticateCallbackComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
571
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.1.4", type: AuthenticateCallbackComponent, isStandalone: true, selector: "haloduck-authenticate-callback", providers: [provideTranslocoScope('haloduck')], ngImport: i0, template: `
572
+ <div
573
+ class="flex flex-col items-center justify-center min-h-screen bg-light-background dark:bg-dark-background"
574
+ >
575
+ <div
576
+ class="w-full max-w-md mx-auto p-8 shadow-lg rounded-2xl bg-light-background dark:bg-dark-background"
577
+ >
578
+ <div class="text-center">
579
+ @if (isProcessing) {
580
+ <div class="space-y-6">
581
+ <div class="flex justify-center">
582
+ <div
583
+ class="animate-spin rounded-full h-16 w-16 border-4 border-gray-300 border-t-blue-600 dark:border-gray-600 dark:border-t-blue-400"
584
+ ></div>
585
+ </div>
586
+ <div class="space-y-2">
587
+ <h2
588
+ class="text-xl font-semibold text-light-on-background dark:text-dark-on-background"
589
+ >
590
+ {{ 'haloduck.ui.authenticate.Processing authentication' | transloco }}
591
+ </h2>
592
+ <p class="text-gray-600 dark:text-gray-300">
593
+ {{ 'haloduck.ui.authenticate.Processing social login...' | transloco }}
594
+ </p>
595
+ </div>
596
+ </div>
597
+ }
598
+
599
+ @if (error) {
600
+ <div class="space-y-6">
601
+ <div class="flex justify-center">
602
+ <div
603
+ class="w-16 h-16 mx-auto mb-4 flex items-center justify-center rounded-full bg-red-100 dark:bg-red-900/20"
604
+ >
605
+ <svg
606
+ class="w-8 h-8 text-red-600 dark:text-red-400"
607
+ fill="currentColor"
608
+ viewBox="0 0 20 20"
609
+ >
610
+ <path
611
+ fill-rule="evenodd"
612
+ d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z"
613
+ clip-rule="evenodd"
614
+ ></path>
615
+ </svg>
616
+ </div>
617
+ </div>
618
+ <div class="space-y-4">
619
+ <h2
620
+ class="text-xl font-semibold text-light-on-background dark:text-dark-on-background"
621
+ >
622
+ {{ 'haloduck.ui.authenticate.Authentication failed' | transloco }}
623
+ </h2>
624
+ <p class="text-gray-600 dark:text-gray-300">
625
+ {{ error && error.startsWith('haloduck.ui.') ? (error | transloco) : error }}
626
+ </p>
627
+ <button
628
+ (click)="redirectToLogin()"
629
+ class="w-full inline-flex items-center justify-center px-4 py-3 border border-transparent text-sm font-medium rounded-lg text-white bg-blue-600 hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:focus:ring-blue-400 transition-colors duration-200"
630
+ >
631
+ {{ 'haloduck.ui.authenticate.Back to login page' | transloco }}
632
+ </button>
633
+ </div>
634
+ </div>
635
+ }
636
+
637
+ @if (success) {
638
+ <div class="space-y-6">
639
+ <div class="flex justify-center">
640
+ <div
641
+ class="w-16 h-16 mx-auto mb-4 flex items-center justify-center rounded-full bg-green-100 dark:bg-green-900/20"
642
+ >
643
+ <svg
644
+ class="w-8 h-8 text-green-600 dark:text-green-400"
645
+ fill="currentColor"
646
+ viewBox="0 0 20 20"
647
+ >
648
+ <path
649
+ fill-rule="evenodd"
650
+ d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
651
+ clip-rule="evenodd"
652
+ ></path>
653
+ </svg>
654
+ </div>
655
+ </div>
656
+ <div class="space-y-4">
657
+ <h2
658
+ class="text-xl font-semibold text-light-on-background dark:text-dark-on-background"
659
+ >
660
+ {{ 'haloduck.ui.authenticate.Authentication successful' | transloco }}
661
+ </h2>
662
+ <p class="text-gray-600 dark:text-gray-300">
663
+ {{ 'haloduck.ui.authenticate.Social login completed.' | transloco }}
664
+ </p>
665
+ <div class="text-sm text-gray-500 dark:text-gray-400">
666
+ {{ 'haloduck.ui.authenticate.Redirecting automatically...' | transloco }}
667
+ </div>
668
+ </div>
669
+ </div>
670
+ }
671
+ </div>
672
+ </div>
673
+ </div>
674
+ `, isInline: true, styles: [":host{display:block;width:100%;height:100vh}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: TranslocoModule }, { kind: "pipe", type: i2$1.TranslocoPipe, name: "transloco" }] });
675
+ }
676
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: AuthenticateCallbackComponent, decorators: [{
677
+ type: Component,
678
+ args: [{ selector: 'haloduck-authenticate-callback', standalone: true, imports: [CommonModule, TranslocoModule], providers: [provideTranslocoScope('haloduck')], template: `
679
+ <div
680
+ class="flex flex-col items-center justify-center min-h-screen bg-light-background dark:bg-dark-background"
681
+ >
682
+ <div
683
+ class="w-full max-w-md mx-auto p-8 shadow-lg rounded-2xl bg-light-background dark:bg-dark-background"
684
+ >
685
+ <div class="text-center">
686
+ @if (isProcessing) {
687
+ <div class="space-y-6">
688
+ <div class="flex justify-center">
689
+ <div
690
+ class="animate-spin rounded-full h-16 w-16 border-4 border-gray-300 border-t-blue-600 dark:border-gray-600 dark:border-t-blue-400"
691
+ ></div>
692
+ </div>
693
+ <div class="space-y-2">
694
+ <h2
695
+ class="text-xl font-semibold text-light-on-background dark:text-dark-on-background"
696
+ >
697
+ {{ 'haloduck.ui.authenticate.Processing authentication' | transloco }}
698
+ </h2>
699
+ <p class="text-gray-600 dark:text-gray-300">
700
+ {{ 'haloduck.ui.authenticate.Processing social login...' | transloco }}
701
+ </p>
702
+ </div>
703
+ </div>
704
+ }
705
+
706
+ @if (error) {
707
+ <div class="space-y-6">
708
+ <div class="flex justify-center">
709
+ <div
710
+ class="w-16 h-16 mx-auto mb-4 flex items-center justify-center rounded-full bg-red-100 dark:bg-red-900/20"
711
+ >
712
+ <svg
713
+ class="w-8 h-8 text-red-600 dark:text-red-400"
714
+ fill="currentColor"
715
+ viewBox="0 0 20 20"
716
+ >
717
+ <path
718
+ fill-rule="evenodd"
719
+ d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z"
720
+ clip-rule="evenodd"
721
+ ></path>
722
+ </svg>
723
+ </div>
724
+ </div>
725
+ <div class="space-y-4">
726
+ <h2
727
+ class="text-xl font-semibold text-light-on-background dark:text-dark-on-background"
728
+ >
729
+ {{ 'haloduck.ui.authenticate.Authentication failed' | transloco }}
730
+ </h2>
731
+ <p class="text-gray-600 dark:text-gray-300">
732
+ {{ error && error.startsWith('haloduck.ui.') ? (error | transloco) : error }}
733
+ </p>
734
+ <button
735
+ (click)="redirectToLogin()"
736
+ class="w-full inline-flex items-center justify-center px-4 py-3 border border-transparent text-sm font-medium rounded-lg text-white bg-blue-600 hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:focus:ring-blue-400 transition-colors duration-200"
737
+ >
738
+ {{ 'haloduck.ui.authenticate.Back to login page' | transloco }}
739
+ </button>
740
+ </div>
741
+ </div>
742
+ }
743
+
744
+ @if (success) {
745
+ <div class="space-y-6">
746
+ <div class="flex justify-center">
747
+ <div
748
+ class="w-16 h-16 mx-auto mb-4 flex items-center justify-center rounded-full bg-green-100 dark:bg-green-900/20"
749
+ >
750
+ <svg
751
+ class="w-8 h-8 text-green-600 dark:text-green-400"
752
+ fill="currentColor"
753
+ viewBox="0 0 20 20"
754
+ >
755
+ <path
756
+ fill-rule="evenodd"
757
+ d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
758
+ clip-rule="evenodd"
759
+ ></path>
760
+ </svg>
761
+ </div>
762
+ </div>
763
+ <div class="space-y-4">
764
+ <h2
765
+ class="text-xl font-semibold text-light-on-background dark:text-dark-on-background"
766
+ >
767
+ {{ 'haloduck.ui.authenticate.Authentication successful' | transloco }}
768
+ </h2>
769
+ <p class="text-gray-600 dark:text-gray-300">
770
+ {{ 'haloduck.ui.authenticate.Social login completed.' | transloco }}
771
+ </p>
772
+ <div class="text-sm text-gray-500 dark:text-gray-400">
773
+ {{ 'haloduck.ui.authenticate.Redirecting automatically...' | transloco }}
774
+ </div>
775
+ </div>
776
+ </div>
777
+ }
778
+ </div>
779
+ </div>
780
+ </div>
781
+ `, styles: [":host{display:block;width:100%;height:100vh}\n"] }]
782
+ }] });
347
783
 
348
784
  class SelectDropdownComponent {
349
785
  _filteredOptions = signal([], ...(ngDevMode ? [{ debugName: "_filteredOptions" }] : []));
@@ -2671,7 +3107,7 @@ class FileUploaderComponent {
2671
3107
  multi: true,
2672
3108
  },
2673
3109
  provideTranslocoScope('haloduck'),
2674
- ], ngImport: i0, template: "<div\n class=\"p-4 border border-light-inactive dark:border-dark-inactive rounded-md\"\n [class.drag-over]=\"isDragOver\"\n (dragover)=\"onDragOver($event)\"\n (dragleave)=\"onDragLeave($event)\"\n (drop)=\"onDrop($event)\"\n>\n @if (!isUploading) {\n <label\n for=\"file-upload\"\n class=\"flex flex-col items-center justify-center w-full border-2 border-dashed border-light-inactive dark:border-dark-inactive rounded-md cursor-pointer hover:border-light-secondary dark:border-dark-secondary p-4\"\n >\n <span class=\"text-light-inactive dark:text-dark-inactive\">{{\n 'haloduck.ui.file.Drag and drop files here, or click to select files' | transloco\n }}</span>\n <input\n id=\"file-upload\"\n type=\"file\"\n class=\"hidden\"\n [attr.accept]=\"accept ? accept.join(',') : null\"\n [attr.multiple]=\"multiple ? '' : null\"\n (cancel)=\"$event.stopPropagation()\"\n (change)=\"onFileSelected($event)\"\n />\n </label>\n }\n <!-- Display file list -->\n @if (files.length > 0) {\n <ul class=\"mt-4 space-y-2\">\n @for (file of files; track file.name; let i = $index) {\n <li\n class=\"flex flex-col sm:flex-row items-stretch sm:items-center justify-center sm:justify-between p-2 border border-light-inactive dark:border-dark-inactive rounded-md gap-2\"\n >\n <!-- Check if the file is an image -->\n <div class=\"flex items-center space-x-4\">\n @if (file.previewUrl) {\n <img\n [src]=\"file.previewUrl\"\n alt=\"{{ file.name }}\"\n class=\"w-12 h-12 object-cover rounded-md\"\n />\n }\n <span class=\"text-sm text-light-inactive dark:text-dark-inactive\">{{ file.name }}</span>\n </div>\n <div class=\"flex items-center space-x-4\">\n <div class=\"flex-1\">\n <haloduck-tag-input\n placeholder=\"{{ 'haloduck.ui.tag.Please input tags.' | transloco }}\"\n [(value)]=\"file.tag\"\n (valueChange)=\"onFileTagChanged(i, $event)\"\n ></haloduck-tag-input>\n </div>\n @if (isUploading) {\n @if (file.isUploaded) {\n <span class=\"text-sm text-light-secondary dark:text-dark-secondary\">{{\n 'haloduck.ui.file.Uploaded' | transloco\n }}</span>\n } @else {\n <span class=\"text-sm text-light-primary dark:text-dark-primary\">{{\n 'haloduck.ui.file.Uploading...' | transloco\n }}</span>\n }\n } @else {\n <button\n type=\"button\"\n class=\"text-light-danger dark:text-dark-danger hover:brightness-125\"\n (click)=\"removeFile(i)\"\n >\n {{ 'haloduck.ui.file.Remove' | transloco }}\n </button>\n }\n </div>\n </li>\n }\n </ul>\n }\n</div>\n", styles: [""], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: TranslocoModule }, { kind: "component", type: TagInputComponent, selector: "haloduck-tag-input", inputs: ["placeholder", "disabled", "allowDuplicates", "value"], outputs: ["valueChange"] }, { kind: "pipe", type: i2$1.TranslocoPipe, name: "transloco" }] });
3110
+ ], ngImport: i0, template: "<div\n class=\"p-4 border border-light-inactive dark:border-dark-inactive rounded-md\"\n [class.drag-over]=\"isDragOver\"\n (dragover)=\"onDragOver($event)\"\n (dragleave)=\"onDragLeave($event)\"\n (drop)=\"onDrop($event)\"\n>\n @if (!isUploading) {\n <label\n for=\"file-upload\"\n class=\"flex flex-col items-center justify-center w-full border-2 border-dashed border-light-inactive dark:border-dark-inactive rounded-md cursor-pointer hover:border-light-secondary dark:border-dark-secondary p-4\"\n >\n <span class=\"text-light-inactive dark:text-dark-inactive\">{{\n 'haloduck.ui.file.Drag and drop files here, or click to select files' | transloco\n }}</span>\n <input\n id=\"file-upload\"\n type=\"file\"\n class=\"hidden\"\n [attr.accept]=\"accept ? accept.join(',') : null\"\n [attr.multiple]=\"multiple ? '' : null\"\n (cancel)=\"$event.stopPropagation()\"\n (change)=\"onFileSelected($event)\"\n />\n </label>\n }\n <!-- Display file list -->\n @if (files.length > 0) {\n <ul class=\"mt-4 space-y-2\">\n @for (file of files; track file.name; let i = $index) {\n <li\n class=\"flex flex-col sm:flex-row items-stretch sm:items-center sm:justify-between p-2 border border-light-inactive dark:border-dark-inactive rounded-md gap-2\"\n >\n <!-- Check if the file is an image -->\n <div class=\"flex items-center space-x-4\">\n @if (file.previewUrl) {\n <img\n [src]=\"file.previewUrl\"\n alt=\"{{ file.name }}\"\n class=\"w-12 h-12 object-cover rounded-md\"\n />\n }\n <span class=\"text-sm text-light-inactive dark:text-dark-inactive\">{{ file.name }}</span>\n </div>\n <div class=\"flex items-center space-x-4\">\n <div class=\"flex-1\">\n <haloduck-tag-input\n placeholder=\"{{ 'haloduck.ui.tag.Please input tags.' | transloco }}\"\n [(value)]=\"file.tag\"\n (valueChange)=\"onFileTagChanged(i, $event)\"\n ></haloduck-tag-input>\n </div>\n @if (isUploading) {\n @if (file.isUploaded) {\n <span class=\"text-sm text-light-secondary dark:text-dark-secondary\">{{\n 'haloduck.ui.file.Uploaded' | transloco\n }}</span>\n } @else {\n <span class=\"text-sm text-light-primary dark:text-dark-primary\">{{\n 'haloduck.ui.file.Uploading...' | transloco\n }}</span>\n }\n } @else {\n <button\n type=\"button\"\n class=\"text-light-danger dark:text-dark-danger hover:brightness-125\"\n (click)=\"removeFile(i)\"\n >\n {{ 'haloduck.ui.file.Remove' | transloco }}\n </button>\n }\n </div>\n </li>\n }\n </ul>\n }\n</div>\n", styles: [""], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: TranslocoModule }, { kind: "component", type: TagInputComponent, selector: "haloduck-tag-input", inputs: ["placeholder", "disabled", "allowDuplicates", "value"], outputs: ["valueChange"] }, { kind: "pipe", type: i2$1.TranslocoPipe, name: "transloco" }] });
2675
3111
  }
2676
3112
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: FileUploaderComponent, decorators: [{
2677
3113
  type: Component,
@@ -2682,7 +3118,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.4", ngImpor
2682
3118
  multi: true,
2683
3119
  },
2684
3120
  provideTranslocoScope('haloduck'),
2685
- ], template: "<div\n class=\"p-4 border border-light-inactive dark:border-dark-inactive rounded-md\"\n [class.drag-over]=\"isDragOver\"\n (dragover)=\"onDragOver($event)\"\n (dragleave)=\"onDragLeave($event)\"\n (drop)=\"onDrop($event)\"\n>\n @if (!isUploading) {\n <label\n for=\"file-upload\"\n class=\"flex flex-col items-center justify-center w-full border-2 border-dashed border-light-inactive dark:border-dark-inactive rounded-md cursor-pointer hover:border-light-secondary dark:border-dark-secondary p-4\"\n >\n <span class=\"text-light-inactive dark:text-dark-inactive\">{{\n 'haloduck.ui.file.Drag and drop files here, or click to select files' | transloco\n }}</span>\n <input\n id=\"file-upload\"\n type=\"file\"\n class=\"hidden\"\n [attr.accept]=\"accept ? accept.join(',') : null\"\n [attr.multiple]=\"multiple ? '' : null\"\n (cancel)=\"$event.stopPropagation()\"\n (change)=\"onFileSelected($event)\"\n />\n </label>\n }\n <!-- Display file list -->\n @if (files.length > 0) {\n <ul class=\"mt-4 space-y-2\">\n @for (file of files; track file.name; let i = $index) {\n <li\n class=\"flex flex-col sm:flex-row items-stretch sm:items-center justify-center sm:justify-between p-2 border border-light-inactive dark:border-dark-inactive rounded-md gap-2\"\n >\n <!-- Check if the file is an image -->\n <div class=\"flex items-center space-x-4\">\n @if (file.previewUrl) {\n <img\n [src]=\"file.previewUrl\"\n alt=\"{{ file.name }}\"\n class=\"w-12 h-12 object-cover rounded-md\"\n />\n }\n <span class=\"text-sm text-light-inactive dark:text-dark-inactive\">{{ file.name }}</span>\n </div>\n <div class=\"flex items-center space-x-4\">\n <div class=\"flex-1\">\n <haloduck-tag-input\n placeholder=\"{{ 'haloduck.ui.tag.Please input tags.' | transloco }}\"\n [(value)]=\"file.tag\"\n (valueChange)=\"onFileTagChanged(i, $event)\"\n ></haloduck-tag-input>\n </div>\n @if (isUploading) {\n @if (file.isUploaded) {\n <span class=\"text-sm text-light-secondary dark:text-dark-secondary\">{{\n 'haloduck.ui.file.Uploaded' | transloco\n }}</span>\n } @else {\n <span class=\"text-sm text-light-primary dark:text-dark-primary\">{{\n 'haloduck.ui.file.Uploading...' | transloco\n }}</span>\n }\n } @else {\n <button\n type=\"button\"\n class=\"text-light-danger dark:text-dark-danger hover:brightness-125\"\n (click)=\"removeFile(i)\"\n >\n {{ 'haloduck.ui.file.Remove' | transloco }}\n </button>\n }\n </div>\n </li>\n }\n </ul>\n }\n</div>\n" }]
3121
+ ], template: "<div\n class=\"p-4 border border-light-inactive dark:border-dark-inactive rounded-md\"\n [class.drag-over]=\"isDragOver\"\n (dragover)=\"onDragOver($event)\"\n (dragleave)=\"onDragLeave($event)\"\n (drop)=\"onDrop($event)\"\n>\n @if (!isUploading) {\n <label\n for=\"file-upload\"\n class=\"flex flex-col items-center justify-center w-full border-2 border-dashed border-light-inactive dark:border-dark-inactive rounded-md cursor-pointer hover:border-light-secondary dark:border-dark-secondary p-4\"\n >\n <span class=\"text-light-inactive dark:text-dark-inactive\">{{\n 'haloduck.ui.file.Drag and drop files here, or click to select files' | transloco\n }}</span>\n <input\n id=\"file-upload\"\n type=\"file\"\n class=\"hidden\"\n [attr.accept]=\"accept ? accept.join(',') : null\"\n [attr.multiple]=\"multiple ? '' : null\"\n (cancel)=\"$event.stopPropagation()\"\n (change)=\"onFileSelected($event)\"\n />\n </label>\n }\n <!-- Display file list -->\n @if (files.length > 0) {\n <ul class=\"mt-4 space-y-2\">\n @for (file of files; track file.name; let i = $index) {\n <li\n class=\"flex flex-col sm:flex-row items-stretch sm:items-center sm:justify-between p-2 border border-light-inactive dark:border-dark-inactive rounded-md gap-2\"\n >\n <!-- Check if the file is an image -->\n <div class=\"flex items-center space-x-4\">\n @if (file.previewUrl) {\n <img\n [src]=\"file.previewUrl\"\n alt=\"{{ file.name }}\"\n class=\"w-12 h-12 object-cover rounded-md\"\n />\n }\n <span class=\"text-sm text-light-inactive dark:text-dark-inactive\">{{ file.name }}</span>\n </div>\n <div class=\"flex items-center space-x-4\">\n <div class=\"flex-1\">\n <haloduck-tag-input\n placeholder=\"{{ 'haloduck.ui.tag.Please input tags.' | transloco }}\"\n [(value)]=\"file.tag\"\n (valueChange)=\"onFileTagChanged(i, $event)\"\n ></haloduck-tag-input>\n </div>\n @if (isUploading) {\n @if (file.isUploaded) {\n <span class=\"text-sm text-light-secondary dark:text-dark-secondary\">{{\n 'haloduck.ui.file.Uploaded' | transloco\n }}</span>\n } @else {\n <span class=\"text-sm text-light-primary dark:text-dark-primary\">{{\n 'haloduck.ui.file.Uploading...' | transloco\n }}</span>\n }\n } @else {\n <button\n type=\"button\"\n class=\"text-light-danger dark:text-dark-danger hover:brightness-125\"\n (click)=\"removeFile(i)\"\n >\n {{ 'haloduck.ui.file.Remove' | transloco }}\n </button>\n }\n </div>\n </li>\n }\n </ul>\n }\n</div>\n" }]
2686
3122
  }], propDecorators: { disabled: [{
2687
3123
  type: Input
2688
3124
  }], urlPrefix: [{
@@ -4403,5 +4839,5 @@ const provideHaloduckTransloco = () => provideTranslocoScope({
4403
4839
  * Generated bundle index. Do not edit.
4404
4840
  */
4405
4841
 
4406
- export { AuthenticateComponent, AutoLoadDirective, BreadcrumbComponent, ButtonComponent, CalendarComponent, ConfirmDialogService, CopyButtonComponent, DatePickerComponent, DateRangeComponent, DialogService, DrawCanvasComponent, ERROR_NOT_ACCEPTABLE_FILE_TYPE, ERROR_OVER_COUNT, ERROR_OVER_SIZE, ERROR_UPLOAD, FileUploaderComponent, FlipComponent, GroupedDirective, ImageUploaderComponent, ImageViewerComponent, InputComponent, LanguageSelectorComponent, MapToAddressComponent, NotificationComponent, NotificationService, PictureNameComponent, SelectComponent, SelectDropdownComponent, SideMenuComponent, SideMenuItemComponent, StlViewerComponent, TableComponent, TableSettingComponent, TableSettingService, TabsComponent, TagInputComponent, TagViewerComponent, ToggleComponent, dateToString, provideHaloduckTransloco };
4842
+ export { AuthenticateCallbackComponent, AuthenticateComponent, AutoLoadDirective, BreadcrumbComponent, ButtonComponent, CalendarComponent, ConfirmDialogService, CopyButtonComponent, DatePickerComponent, DateRangeComponent, DialogService, DrawCanvasComponent, ERROR_NOT_ACCEPTABLE_FILE_TYPE, ERROR_OVER_COUNT, ERROR_OVER_SIZE, ERROR_UPLOAD, FileUploaderComponent, FlipComponent, GroupedDirective, ImageUploaderComponent, ImageViewerComponent, InputComponent, LanguageSelectorComponent, MapToAddressComponent, NotificationComponent, NotificationService, PictureNameComponent, SelectComponent, SelectDropdownComponent, SideMenuComponent, SideMenuItemComponent, StlViewerComponent, TableComponent, TableSettingComponent, TableSettingService, TabsComponent, TagInputComponent, TagViewerComponent, ToggleComponent, dateToString, provideHaloduckTransloco };
4407
4843
  //# sourceMappingURL=haloduck-ui.mjs.map