@evenicanpm/storefront-core 2.4.0 → 2.4.1

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.
Files changed (36) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/package.json +15 -2
  3. package/src/api-manager/datasources/d365/d365-address.datasource.ts +142 -66
  4. package/src/api-manager/datasources/d365/d365-user.datasource.ts +109 -37
  5. package/src/api-manager/datasources/d365/utils/get-context-cookie.ts +44 -10
  6. package/src/api-manager/lib/get-graphql-client.ts +35 -7
  7. package/src/api-manager/services/create-query.ts +36 -3
  8. package/src/api-manager/services/get-query-client.ts +10 -0
  9. package/src/auth/better-auth.ts +282 -15
  10. package/src/cms/blocks/components/product-section-fullwidth/index.tsx +3 -1
  11. package/src/components/categories/category-list/category-list.tsx +9 -5
  12. package/src/components/header/__tests__/user.test.tsx +5 -107
  13. package/src/components/header/components/user.tsx +72 -45
  14. package/src/hooks/use-nextauth-session.ts +0 -33
  15. package/src/lib/auth-client.ts +14 -0
  16. package/src/lib/auth.ts +4 -0
  17. package/src/lib/entra-native-auth.ts +138 -0
  18. package/src/lib/native-session.ts +434 -0
  19. package/src/pages/account/profile/__tests__/profile-form.test.tsx +173 -0
  20. package/src/pages/account/profile/profile-form.tsx +285 -0
  21. package/src/pages/account/profile/profile-validation.ts +52 -0
  22. package/src/pages/auth/auth-activate-page.tsx +509 -0
  23. package/src/pages/auth/auth-layout.tsx +73 -0
  24. package/src/pages/auth/auth-login-page.tsx +241 -0
  25. package/src/pages/auth/auth-reset-password-page.tsx +487 -0
  26. package/src/pages/auth/compound/auth-form.tsx +182 -0
  27. package/src/pages/auth/compound/auth-pages.tsx +21 -0
  28. package/src/pages/auth/lib/friendly-error.ts +70 -0
  29. package/src/pages/auth/lib/index.ts +2 -0
  30. package/src/pages/auth/lib/types.ts +5 -0
  31. package/src/pages/checkout/checkout-alt-form/steps/address/delivery-address.tsx +20 -6
  32. package/src/pages/checkout/checkout-alt-form/steps/customer-info/customer-information.tsx +1 -1
  33. package/tsconfig.json +20 -14
  34. package/src/auth/msal.ts +0 -65
  35. package/src/pages/account/profile/profile-button.test.tsx +0 -59
  36. package/src/pages/account/profile/profile-button.tsx +0 -51
@@ -0,0 +1,182 @@
1
+ "use client";
2
+
3
+ import {
4
+ Box,
5
+ type BoxProps,
6
+ Button,
7
+ CircularProgress,
8
+ TextField,
9
+ } from "@mui/material";
10
+ import type { ChangeEvent, FC } from "react";
11
+
12
+ interface AuthFormSubmitButtonProps {
13
+ label: string;
14
+ loading: boolean;
15
+ disabled?: boolean;
16
+ color?: "primary" | "secondary";
17
+ }
18
+
19
+ interface AuthFormPasswordFieldsProps {
20
+ password: string;
21
+ confirmPassword: string;
22
+ onPasswordChange: (value: string) => void;
23
+ onConfirmChange: (value: string) => void;
24
+ disabled?: boolean;
25
+ autoFocus?: boolean;
26
+ label?: string;
27
+ confirmLabel?: string;
28
+ mismatchHelperText?: string;
29
+ }
30
+
31
+ interface AuthFormOtpFieldProps {
32
+ value: string;
33
+ onChange: (value: string) => void;
34
+ codeLength: number;
35
+ disabled?: boolean;
36
+ autoFocus?: boolean;
37
+ label?: string;
38
+ }
39
+
40
+ interface AuthFormErrorProps {
41
+ message: string | null;
42
+ }
43
+
44
+ const AuthFormError = ({ message }: AuthFormErrorProps) => {
45
+ if (!message) return null;
46
+
47
+ return (
48
+ <Box
49
+ role="alert"
50
+ sx={{
51
+ mb: 2,
52
+ color: "error.main",
53
+ fontSize: "0.875rem",
54
+ }}
55
+ >
56
+ {message}
57
+ </Box>
58
+ );
59
+ };
60
+
61
+ const AuthFormSubmitButton = ({
62
+ label,
63
+ loading,
64
+ disabled = false,
65
+ color = "primary",
66
+ }: AuthFormSubmitButtonProps) => {
67
+ return (
68
+ <Button
69
+ type="submit"
70
+ variant="contained"
71
+ color={color}
72
+ fullWidth
73
+ size="large"
74
+ disabled={loading || disabled}
75
+ sx={{ borderRadius: 2, py: 1.4 }}
76
+ >
77
+ {loading ? <CircularProgress size={20} color="inherit" /> : label}
78
+ </Button>
79
+ );
80
+ };
81
+
82
+ const AuthFormPasswordFields = ({
83
+ password,
84
+ confirmPassword,
85
+ onPasswordChange,
86
+ onConfirmChange,
87
+ disabled = false,
88
+ autoFocus = false,
89
+ label = "Password",
90
+ confirmLabel = "Confirm password",
91
+ mismatchHelperText = "Passwords do not match",
92
+ }: AuthFormPasswordFieldsProps) => {
93
+ const mismatch = confirmPassword.length > 0 && password !== confirmPassword;
94
+
95
+ return (
96
+ <>
97
+ <TextField
98
+ label={label}
99
+ type="password"
100
+ value={password}
101
+ onChange={(
102
+ event: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
103
+ ) => onPasswordChange(event.target.value)}
104
+ fullWidth
105
+ required
106
+ autoComplete="new-password"
107
+ autoFocus={autoFocus}
108
+ sx={{ mb: 2 }}
109
+ disabled={disabled}
110
+ />
111
+ <TextField
112
+ label={confirmLabel}
113
+ type="password"
114
+ value={confirmPassword}
115
+ onChange={(
116
+ event: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
117
+ ) => onConfirmChange(event.target.value)}
118
+ fullWidth
119
+ required
120
+ autoComplete="new-password"
121
+ error={mismatch}
122
+ helperText={mismatch ? mismatchHelperText : undefined}
123
+ sx={{ mb: 3 }}
124
+ disabled={disabled}
125
+ />
126
+ </>
127
+ );
128
+ };
129
+
130
+ const AuthFormOtpField = ({
131
+ value,
132
+ onChange,
133
+ codeLength,
134
+ disabled = false,
135
+ autoFocus = true,
136
+ label = "Verification code",
137
+ }: AuthFormOtpFieldProps) => {
138
+ return (
139
+ <TextField
140
+ label={label}
141
+ type="text"
142
+ value={value}
143
+ onChange={(event: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) =>
144
+ onChange(event.target.value.replace(/\D/g, "").slice(0, codeLength))
145
+ }
146
+ fullWidth
147
+ required
148
+ autoFocus={autoFocus}
149
+ autoComplete="one-time-code"
150
+ inputProps={{
151
+ maxLength: codeLength,
152
+ style: {
153
+ textAlign: "center",
154
+ letterSpacing: "0.4em",
155
+ fontSize: "1.3rem",
156
+ },
157
+ }}
158
+ sx={{ mb: 3 }}
159
+ disabled={disabled}
160
+ />
161
+ );
162
+ };
163
+
164
+ type AuthFormProps = BoxProps<"form">;
165
+
166
+ type AuthFormComponent = FC<AuthFormProps> & {
167
+ Error: typeof AuthFormError;
168
+ SubmitButton: typeof AuthFormSubmitButton;
169
+ PasswordFields: typeof AuthFormPasswordFields;
170
+ OtpField: typeof AuthFormOtpField;
171
+ };
172
+
173
+ const AuthForm = ((props: AuthFormProps) => (
174
+ <Box component="form" noValidate {...props} />
175
+ )) as AuthFormComponent;
176
+
177
+ AuthForm.Error = AuthFormError;
178
+ AuthForm.SubmitButton = AuthFormSubmitButton;
179
+ AuthForm.PasswordFields = AuthFormPasswordFields;
180
+ AuthForm.OtpField = AuthFormOtpField;
181
+
182
+ export default AuthForm;
@@ -0,0 +1,21 @@
1
+ "use client";
2
+
3
+ import AuthActivatePage from "@evenicanpm/storefront-core/src/pages/auth/auth-activate-page";
4
+ import AuthLoginPage from "@evenicanpm/storefront-core/src/pages/auth/auth-login-page";
5
+ import AuthResetPasswordPage from "@evenicanpm/storefront-core/src/pages/auth/auth-reset-password-page";
6
+
7
+ type AuthPagesComponent = ((props: { children?: unknown }) => unknown) & {
8
+ Login: typeof AuthLoginPage;
9
+ Activate: typeof AuthActivatePage;
10
+ ResetPassword: typeof AuthResetPasswordPage;
11
+ };
12
+
13
+ const AuthPages = (({ children }: { children?: unknown }) => (
14
+ <>{children}</>
15
+ )) as AuthPagesComponent;
16
+
17
+ AuthPages.Login = AuthLoginPage;
18
+ AuthPages.Activate = AuthActivatePage;
19
+ AuthPages.ResetPassword = AuthResetPasswordPage;
20
+
21
+ export default AuthPages;
@@ -0,0 +1,70 @@
1
+ type Translate = (key: string) => string;
2
+ const genericMessage = "Something went wrong. Please try again.";
3
+
4
+ const defaultMessages: Record<string, string> = {
5
+ "backend.passwordTooWeak": "Password is too weak.",
6
+ "backend.passwordTooShort": "Password is too short.",
7
+ "backend.passwordTooLong": "Password is too long.",
8
+ "backend.passwordRecentlyUsed": "Password was recently used.",
9
+ "backend.passwordBanned": "Password is not allowed.",
10
+ "backend.passwordInvalid": "Password is invalid.",
11
+ "backend.invalidVerificationCode": "Verification code is invalid.",
12
+ "backend.userAlreadyExists": "An account already exists.",
13
+ "backend.userNotFound": "No account found for this email.",
14
+ "backend.redirectRequired":
15
+ "This tenant requires browser-based authentication.",
16
+ "backend.resetFailed": "Password reset failed.",
17
+ "backend.timeout": "Request timed out. Please try again.",
18
+ generic: genericMessage,
19
+ };
20
+
21
+ const translateOrDefault = (t: Translate | undefined, key: string): string => {
22
+ if (t) return t(key);
23
+
24
+ return defaultMessages[key] ?? genericMessage;
25
+ };
26
+
27
+ export function friendlyError(
28
+ error: string,
29
+ suberror?: string,
30
+ t?: Translate,
31
+ ): string {
32
+ if (suberror === "password_too_weak") {
33
+ return translateOrDefault(t, "backend.passwordTooWeak");
34
+ }
35
+ if (suberror === "password_too_short") {
36
+ return translateOrDefault(t, "backend.passwordTooShort");
37
+ }
38
+ if (suberror === "password_too_long") {
39
+ return translateOrDefault(t, "backend.passwordTooLong");
40
+ }
41
+ if (suberror === "password_recently_used") {
42
+ return translateOrDefault(t, "backend.passwordRecentlyUsed");
43
+ }
44
+ if (suberror === "password_banned") {
45
+ return translateOrDefault(t, "backend.passwordBanned");
46
+ }
47
+ if (suberror === "password_is_invalid") {
48
+ return translateOrDefault(t, "backend.passwordInvalid");
49
+ }
50
+ if (suberror === "invalid_oob_value") {
51
+ return translateOrDefault(t, "backend.invalidVerificationCode");
52
+ }
53
+ if (error === "user_already_exists") {
54
+ return translateOrDefault(t, "backend.userAlreadyExists");
55
+ }
56
+ if (error === "user_not_found") {
57
+ return translateOrDefault(t, "backend.userNotFound");
58
+ }
59
+ if (error === "redirect_required") {
60
+ return translateOrDefault(t, "backend.redirectRequired");
61
+ }
62
+ if (error === "reset_failed") {
63
+ return translateOrDefault(t, "backend.resetFailed");
64
+ }
65
+ if (error === "timeout") {
66
+ return translateOrDefault(t, "backend.timeout");
67
+ }
68
+
69
+ return translateOrDefault(t, "generic");
70
+ }
@@ -0,0 +1,2 @@
1
+ export { friendlyError } from "@evenicanpm/storefront-core/src/pages/auth/lib/friendly-error";
2
+ export type { OtpMeta } from "@evenicanpm/storefront-core/src/pages/auth/lib/types";
@@ -0,0 +1,5 @@
1
+ export interface OtpMeta {
2
+ continuation_token: string;
3
+ challenge_target_label: string;
4
+ code_length: number;
5
+ }
@@ -105,7 +105,8 @@ const DeliveryAddress: DeliveryAddressCompound = ({
105
105
 
106
106
  // user + guest logic
107
107
  const { data: user, isLoading: userLoading } = getUser.useData();
108
- const isGuest = !userLoading && !user?.RecordId;
108
+ const isAuthenticatedUser = !!user;
109
+ const isGuest = !userLoading && !isAuthenticatedUser;
109
110
 
110
111
  // dropdown visibility
111
112
  const showDropDown = useDropDown && !isGuest;
@@ -165,9 +166,22 @@ const DeliveryAddress: DeliveryAddressCompound = ({
165
166
 
166
167
  const handleAuthFlow = async (address: Address) => {
167
168
  const isNew = !address.Id;
168
- const saved = isNew
169
- ? await createAddress(address)
170
- : await updateAddress(address);
169
+ let saved: Address;
170
+ if (isNew) {
171
+ try {
172
+ saved = await createAddress(address);
173
+ } catch (error) {
174
+ const message = error instanceof Error ? error.message : String(error);
175
+ // Newly created async customers may not be ready for address persistence yet.
176
+ if (message.includes("CreatingAsyncAddressesNotSupported")) {
177
+ saved = address;
178
+ } else {
179
+ throw error;
180
+ }
181
+ }
182
+ } else {
183
+ saved = await updateAddress(address);
184
+ }
171
185
 
172
186
  // update local list
173
187
  if (isNew) {
@@ -184,12 +198,12 @@ const DeliveryAddress: DeliveryAddressCompound = ({
184
198
  };
185
199
 
186
200
  const handleSaveAddress = async (address: Address) => {
187
- if (isGuest) return handleGuestFlow(address);
201
+ if (!isAuthenticatedUser || userLoading) return handleGuestFlow(address);
188
202
  return handleAuthFlow(address);
189
203
  };
190
204
 
191
205
  const handleDelete = async (address: Address) => {
192
- if (!isGuest && address.Id) {
206
+ if (isAuthenticatedUser && address.Id) {
193
207
  await deleteAddress(address.Id);
194
208
  }
195
209
  setAddressList((prev) => prev.filter((a) => a.Id !== address.Id));
@@ -39,7 +39,7 @@ const CustomerInformation = (
39
39
  } = props;
40
40
 
41
41
  const { data: user, isLoading: userLoading } = getUser.useData();
42
- const isGuest = !userLoading && !user?.RecordId;
42
+ const isGuest = !userLoading && !user;
43
43
 
44
44
  const resolveEmail = () => {
45
45
  if (isGuest) return cartEmail;
package/tsconfig.json CHANGED
@@ -7,25 +7,31 @@
7
7
  "esModuleInterop": true,
8
8
  "skipLibCheck": true,
9
9
  "declaration": true,
10
- "moduleResolution": "node",
10
+ "moduleResolution": "bundler",
11
11
  "resolveJsonModule": true,
12
12
  "outDir": "./dist",
13
+ "rootDir": "./src",
13
14
  "jsx": "preserve",
14
- "baseUrl": ".",
15
15
  "paths": {
16
+ "@evenicanpm/cms/src/*": ["../cms/src/*"],
16
17
  "@evenicanpm/storefront-core/src/*": ["./src/*"],
17
- "@evenicanpm/storefront-core/src/lib/*": ["src/lib/*"],
18
- "@evenicanpm/storefront-core/src/api-manager/*": ["src/api-manager/*"],
19
- "@evenicanpm/storefront-core/src/components/*": ["src/components/*"],
20
- "@/auth/*": ["src/auth/*"],
21
- "@/_components/*": ["src/components/_components/*"],
22
- "@/components/*": ["src/components/*"],
23
- "@/cms/*": ["src/cms/*"],
24
- "@/schemas/*": ["src/api-manager/schemas/*"],
25
- "@/services/*": ["src/api-manager/services/*"],
26
- "@evenicanpm/storefront-core/src/hooks/*": ["src/hooks/*"],
27
- "@config": ["src/config"],
28
- "@messages/*": ["../../../../storefront/messages/*"]
18
+ "@evenicanpm/storefront-core/src/lib/*": ["./src/lib/*"],
19
+ "@evenicanpm/storefront-core/src/api-manager/*": ["./src/api-manager/*"],
20
+ "@evenicanpm/storefront-core/src/components/*": ["./src/components/*"],
21
+ "@/auth/*": ["./src/auth/*"],
22
+ "@/_components/*": ["./src/components/_components/*"],
23
+ "@/components/*": ["./src/components/*"],
24
+ "@/cms/*": ["./src/cms/*"],
25
+ "@/schemas/*": ["./src/api-manager/schemas/*"],
26
+ "@/services/*": ["./src/api-manager/services/*"],
27
+ "@evenicanpm/storefront-core/src/hooks/*": ["./src/hooks/*"],
28
+ "@config": ["./src/config"],
29
+ "@messages/*": ["../../../../storefront/messages/*"],
30
+ "*": [
31
+ "../../node_modules/.pnpm/node_modules/*",
32
+ "./node_modules/*",
33
+ "../../apps/storefront/node_modules/*"
34
+ ]
29
35
  }
30
36
  },
31
37
  "include": ["src"]
package/src/auth/msal.ts DELETED
@@ -1,65 +0,0 @@
1
- import {
2
- type Configuration,
3
- PublicClientApplication,
4
- } from "@azure/msal-browser";
5
-
6
- const createMsalConfig = (): Configuration => {
7
- return {
8
- auth: {
9
- clientId: process.env.NEXT_PUBLIC_AZURE_AD_B2C_CLIENTID || "",
10
- authority: process.env.NEXT_PUBLIC_AZURE_AD_B2C_LOGIN_AUTHORITY || "",
11
- redirectUri: process.env.NEXT_PUBLIC_AZURE_AD_B2C_REDIRECT || "/",
12
- postLogoutRedirectUri:
13
- process.env.NEXT_PUBLIC_AZURE_AD_B2C_POST_LOGOUT_REDIRECT || "/",
14
- knownAuthorities: [
15
- process.env.NEXT_PUBLIC_AZURE_AD_B2C_KNOWN_AUTHORITY || "",
16
- ],
17
- },
18
- system: {},
19
- };
20
- };
21
-
22
- // create module singleton (to pass the env variables)
23
- // const msalInstance = new PublicClientApplication(createMsalConfig());
24
- let msalInstance: PublicClientApplication | null = null;
25
-
26
- const initializeMsal = async () => {
27
- try {
28
- const config = createMsalConfig();
29
- msalInstance = new PublicClientApplication(config);
30
- await msalInstance.initialize();
31
-
32
- const accounts = msalInstance.getAllAccounts();
33
- if (accounts.length > 0) {
34
- msalInstance.setActiveAccount(accounts[0]);
35
- }
36
- } catch (error) {
37
- console.error("MSAL Initialization Error:", error);
38
- }
39
- };
40
-
41
- const getMsalInstance = () => {
42
- if (!msalInstance) throw new Error("msalInstance not initialized");
43
- return msalInstance;
44
- };
45
-
46
- const getRequestConfig = (requestType?: string) => {
47
- const editAuthority = process.env.NEXT_PUBLIC_AZURE_AD_B2C_EDIT_AUTHORITY;
48
- const resetAuthority = process.env.NEXT_PUBLIC_AZURE_AD_B2C_RESET_AUTHORITY;
49
-
50
- const baseRequest = {
51
- scopes: [process.env.NEXT_PUBLIC_AZURE_AD_B2C_API_PERMISSION],
52
- authority: createMsalConfig().auth.authority,
53
- };
54
-
55
- switch (requestType) {
56
- case "edit":
57
- return { ...baseRequest, authority: editAuthority };
58
- case "reset":
59
- return { ...baseRequest, authority: resetAuthority };
60
- default:
61
- return baseRequest;
62
- }
63
- };
64
-
65
- export { getMsalInstance, getRequestConfig, initializeMsal };
@@ -1,59 +0,0 @@
1
- import ProfileButton from "@evenicanpm/storefront-core/src/pages/account/profile/profile-button";
2
- import { fireEvent, render, screen, waitFor } from "@testing-library/react";
3
- import { vi } from "vitest";
4
- import { getMsalInstance, getRequestConfig, initializeMsal } from "@/auth/msal";
5
-
6
- vi.mock("@/auth/msal", () => ({
7
- initializeMsal: vi.fn(),
8
- getMsalInstance: vi.fn(),
9
- getRequestConfig: vi.fn(),
10
- }));
11
-
12
- vi.mock("@evenicanpm/storefront-core/src/components/T/index", () => ({
13
- default: ({ path }: { path: string }) => <span>{path}</span>,
14
- }));
15
-
16
- describe("ProfileButton Component", () => {
17
- afterEach(() => {
18
- vi.restoreAllMocks();
19
- });
20
-
21
- it("renders disabled button initially (MSAL not ready)", () => {
22
- render(<ProfileButton type="edit" path="Account.Profile.btnEditProfile" />);
23
- const button = screen.getByRole("button");
24
- expect(button).toBeDisabled();
25
- });
26
-
27
- it("initializes MSAL on mount", async () => {
28
- render(<ProfileButton type="edit" path="Account.Profile.btnEditProfile" />);
29
- await waitFor(() => {
30
- expect(initializeMsal).toHaveBeenCalled();
31
- });
32
- });
33
-
34
- it("calls MSAL loginRedirect when clicked", async () => {
35
- (initializeMsal as ReturnType<typeof vi.fn>).mockImplementation(() =>
36
- Promise.resolve(),
37
- );
38
- (getRequestConfig as ReturnType<typeof vi.fn>).mockReturnValue({
39
- scopes: ["user.read"],
40
- });
41
- (getMsalInstance as ReturnType<typeof vi.fn>).mockReturnValue({
42
- loginRedirect: vi.fn(),
43
- });
44
- render(<ProfileButton type="edit" path="Account.Profile.btnEditProfile" />);
45
-
46
- await waitFor(() => {
47
- expect(initializeMsal).toHaveBeenCalled();
48
- });
49
-
50
- const button = screen.getByRole("button");
51
- fireEvent.click(button);
52
-
53
- await waitFor(() => {
54
- expect(getMsalInstance().loginRedirect).toHaveBeenCalledWith({
55
- scopes: ["user.read"],
56
- });
57
- });
58
- });
59
- });
@@ -1,51 +0,0 @@
1
- "use client";
2
-
3
- import type { RedirectRequest } from "@azure/msal-browser";
4
- import T from "@evenicanpm/storefront-core/src/components/T/index";
5
- import { Button } from "@mui/material";
6
- import { useEffect, useState } from "react";
7
- import { getMsalInstance, getRequestConfig, initializeMsal } from "@/auth/msal";
8
-
9
- interface Props {
10
- type: string;
11
- path: string;
12
- }
13
-
14
- const ProfileButton = ({ type, path }: Props) => {
15
- const [isMsalReady, setIsMsalReady] = useState(false);
16
-
17
- useEffect(() => {
18
- const initMsal = async () => {
19
- await initializeMsal();
20
- setIsMsalReady(true);
21
- };
22
- initMsal();
23
- }, []);
24
-
25
- const handleProfile = async () => {
26
- if (!isMsalReady) {
27
- console.error("MSAL is not initialized yet.");
28
- return;
29
- }
30
-
31
- try {
32
- const req = getRequestConfig(type);
33
- await getMsalInstance().loginRedirect(req as RedirectRequest); // "handleRedirectPromise" is in user component.
34
- } catch (error) {
35
- console.error("Profile update through MSAL failed", error);
36
- }
37
- };
38
-
39
- return (
40
- <Button
41
- onClick={() => handleProfile()}
42
- disabled={!isMsalReady}
43
- color="primary"
44
- sx={{ bgcolor: "primary.light", px: 4 }}
45
- >
46
- <T path={path} />
47
- </Button>
48
- );
49
- };
50
-
51
- export default ProfileButton;