@churchapps/apphelper-login 0.4.13

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 (73) hide show
  1. package/README.md +104 -0
  2. package/dist/LoginPage.d.ts +25 -0
  3. package/dist/LoginPage.d.ts.map +1 -0
  4. package/dist/LoginPage.js +248 -0
  5. package/dist/LoginPage.js.map +1 -0
  6. package/dist/LogoutPage.d.ts +9 -0
  7. package/dist/LogoutPage.d.ts.map +1 -0
  8. package/dist/LogoutPage.js +33 -0
  9. package/dist/LogoutPage.js.map +1 -0
  10. package/dist/components/Forgot.d.ts +8 -0
  11. package/dist/components/Forgot.d.ts.map +1 -0
  12. package/dist/components/Forgot.js +127 -0
  13. package/dist/components/Forgot.js.map +1 -0
  14. package/dist/components/Login.d.ts +15 -0
  15. package/dist/components/Login.d.ts.map +1 -0
  16. package/dist/components/Login.js +126 -0
  17. package/dist/components/Login.js.map +1 -0
  18. package/dist/components/LoginSetPassword.d.ts +13 -0
  19. package/dist/components/LoginSetPassword.d.ts.map +1 -0
  20. package/dist/components/LoginSetPassword.js +167 -0
  21. package/dist/components/LoginSetPassword.js.map +1 -0
  22. package/dist/components/Register.d.ts +12 -0
  23. package/dist/components/Register.d.ts.map +1 -0
  24. package/dist/components/Register.js +217 -0
  25. package/dist/components/Register.js.map +1 -0
  26. package/dist/components/SelectChurchModal.d.ts +14 -0
  27. package/dist/components/SelectChurchModal.d.ts.map +1 -0
  28. package/dist/components/SelectChurchModal.js +39 -0
  29. package/dist/components/SelectChurchModal.js.map +1 -0
  30. package/dist/components/SelectChurchRegister.d.ts +11 -0
  31. package/dist/components/SelectChurchRegister.d.ts.map +1 -0
  32. package/dist/components/SelectChurchRegister.js +83 -0
  33. package/dist/components/SelectChurchRegister.js.map +1 -0
  34. package/dist/components/SelectChurchSearch.d.ts +10 -0
  35. package/dist/components/SelectChurchSearch.d.ts.map +1 -0
  36. package/dist/components/SelectChurchSearch.js +61 -0
  37. package/dist/components/SelectChurchSearch.js.map +1 -0
  38. package/dist/components/SelectableChurch.d.ts +9 -0
  39. package/dist/components/SelectableChurch.d.ts.map +1 -0
  40. package/dist/components/SelectableChurch.js +34 -0
  41. package/dist/components/SelectableChurch.js.map +1 -0
  42. package/dist/helpers/AnalyticsHelper.d.ts +7 -0
  43. package/dist/helpers/AnalyticsHelper.d.ts.map +1 -0
  44. package/dist/helpers/AnalyticsHelper.js +30 -0
  45. package/dist/helpers/AnalyticsHelper.js.map +1 -0
  46. package/dist/helpers/Locale.d.ts +13 -0
  47. package/dist/helpers/Locale.d.ts.map +1 -0
  48. package/dist/helpers/Locale.js +200 -0
  49. package/dist/helpers/Locale.js.map +1 -0
  50. package/dist/helpers/index.d.ts +3 -0
  51. package/dist/helpers/index.d.ts.map +1 -0
  52. package/dist/helpers/index.js +3 -0
  53. package/dist/helpers/index.js.map +1 -0
  54. package/dist/index.d.ts +11 -0
  55. package/dist/index.d.ts.map +1 -0
  56. package/dist/index.js +11 -0
  57. package/dist/index.js.map +1 -0
  58. package/package.json +57 -0
  59. package/src/LoginPage.tsx +283 -0
  60. package/src/LogoutPage.tsx +43 -0
  61. package/src/components/Forgot.tsx +247 -0
  62. package/src/components/Login.tsx +250 -0
  63. package/src/components/LoginSetPassword.tsx +298 -0
  64. package/src/components/Register.tsx +371 -0
  65. package/src/components/SelectChurchModal.tsx +82 -0
  66. package/src/components/SelectChurchRegister.tsx +88 -0
  67. package/src/components/SelectChurchSearch.tsx +85 -0
  68. package/src/components/SelectableChurch.tsx +72 -0
  69. package/src/helpers/AnalyticsHelper.ts +32 -0
  70. package/src/helpers/Locale.ts +233 -0
  71. package/src/helpers/index.ts +2 -0
  72. package/src/index.ts +10 -0
  73. package/tsconfig.json +30 -0
@@ -0,0 +1,72 @@
1
+ "use client";
2
+
3
+ import { Grid } from "@mui/material";
4
+ import React from "react";
5
+ import { ArrayHelper } from "@churchapps/helpers";
6
+ import { ChurchInterface, GenericSettingInterface } from "@churchapps/helpers";
7
+
8
+ interface Props {
9
+ selectChurch: (churchId: string) => void,
10
+ church: ChurchInterface
11
+ }
12
+
13
+ export const SelectableChurch: React.FC<Props> = (props) => {
14
+
15
+ let logo = "/images/logo.png";
16
+ if (props.church.settings) {
17
+ let l: GenericSettingInterface = ArrayHelper.getOne(props.church.settings, "keyName", "logoLight");
18
+ if (l?.value) logo = l.value;
19
+ }
20
+ return (
21
+ <Grid container spacing={3}>
22
+ <Grid size={{ xs: 12, md: 6 }}>
23
+ <button
24
+ type="button"
25
+ style={{
26
+ fontSize: "1.125rem",
27
+ display: "block",
28
+ marginTop: 15,
29
+ marginBottom: 15,
30
+ background: "none",
31
+ border: "none",
32
+ cursor: "pointer",
33
+ padding: 0,
34
+ width: "100%"
35
+ }}
36
+ onClick={(e) => { e.preventDefault(); props.selectChurch(props.church.id) }}
37
+ aria-label={`Select church: ${props.church.name}`}
38
+ >
39
+ <img src={logo} alt="church logo" className="w-100 img-fluid" />
40
+ </button>
41
+ </Grid>
42
+ <Grid size={{ xs: 12, md: 6 }}>
43
+ <div>
44
+ <button
45
+ type="button"
46
+ style={{
47
+ fontSize: "1.125rem",
48
+ display: "block",
49
+ background: "none",
50
+ border: "none",
51
+ color: "#3b82f6",
52
+ cursor: "pointer",
53
+ textDecoration: "underline",
54
+ padding: 0,
55
+ textAlign: "left"
56
+ }}
57
+ onClick={(e) => { e.preventDefault(); props.selectChurch(props.church.id) }}
58
+ aria-label={`Select church: ${props.church.name}`}
59
+ >
60
+ {props.church.name}
61
+ </button>
62
+ {(props.church.address1) && <div>{props.church.address1}</div>}
63
+ {(props.church.city || props.church.state) && <div>
64
+ {props.church.city && props.church.city + ", "}
65
+ {props.church.state}
66
+ </div>}
67
+ </div>
68
+ </Grid>
69
+ <span style={{ display: "block", width: "100%", borderTop: "1px solid #ccc", margin: "1rem" }}></span>
70
+ </Grid>
71
+ );
72
+ };
@@ -0,0 +1,32 @@
1
+ import ReactGA from "react-ga4";
2
+ import { CommonEnvironmentHelper, UserHelper } from "@churchapps/helpers";
3
+
4
+ export class AnalyticsHelper {
5
+
6
+ static init = () => {
7
+ if (CommonEnvironmentHelper.GoogleAnalyticsTag !== "" && typeof(window)!=="undefined") {
8
+ ReactGA.initialize([{trackingId: CommonEnvironmentHelper.GoogleAnalyticsTag}]);
9
+ AnalyticsHelper.logPageView();
10
+ }
11
+ }
12
+
13
+ static logPageView = () => {
14
+ if (CommonEnvironmentHelper.GoogleAnalyticsTag !== "" && typeof(window)!=="undefined") {
15
+ this.setChurchKey();
16
+ ReactGA.send({ hitType: "pageview", page: window.location.pathname + window.location.search });
17
+ }
18
+ }
19
+
20
+ static logEvent = (category: string, action: string, label?:string) => {
21
+ if (CommonEnvironmentHelper.GoogleAnalyticsTag !== "" && typeof(window)!=="undefined") {
22
+ this.setChurchKey();
23
+ ReactGA.event({ category, action, label });
24
+ }
25
+ }
26
+
27
+ private static setChurchKey = () => {
28
+ const churchKey = UserHelper?.currentUserChurch?.church?.subDomain;
29
+ if (churchKey) ReactGA.set({church_key: churchKey });
30
+ }
31
+
32
+ }
@@ -0,0 +1,233 @@
1
+ import i18n from "i18next";
2
+ import { initReactI18next } from "react-i18next/initReactI18next";
3
+ import LanguageDetector from "i18next-browser-languagedetector";
4
+ import Backend from "i18next-chained-backend";
5
+ import HttpBackend from "i18next-http-backend";
6
+
7
+ interface TranslationResources {
8
+ [key: string]: {
9
+ translation: Record<string, unknown>;
10
+ };
11
+ }
12
+
13
+ interface ExtraLanguageCodes {
14
+ [key: string]: string[];
15
+ }
16
+
17
+ export class Locale {
18
+ private static readonly supportedLanguages: string[] = [
19
+ "de",
20
+ "en",
21
+ "es",
22
+ "fr",
23
+ "hi",
24
+ "it",
25
+ "ko",
26
+ "no",
27
+ "pt",
28
+ "ru",
29
+ "tl",
30
+ "zh",
31
+ ];
32
+ private static readonly extraCodes: ExtraLanguageCodes = { no: ["nb", "nn"] };
33
+
34
+ // English fallbacks for all login-related labels
35
+ private static readonly fallbacks: Record<string, string> = {
36
+ // Common
37
+ "common.pleaseWait": "Please wait...",
38
+ "common.search": "Search",
39
+
40
+ // Login
41
+ "login.createAccount": "Create an Account",
42
+ "login.email": "Email",
43
+ "login.expiredLink": "The current link is expired.",
44
+ "login.forgot": "Forgot Password",
45
+ "login.goLogin": "Go to Login",
46
+ "login.login": "Login",
47
+ "login.password": "Password",
48
+ "login.register": "Register",
49
+ "login.registerThankYou": "Thank you for registering! Please check your email to verify your account.",
50
+ "login.requestLink": "Request a new reset link",
51
+ "login.reset": "Reset",
52
+ "login.resetInstructions": "Enter your email address to request a password reset.",
53
+ "login.resetPassword": "Reset Password",
54
+ "login.resetSent": "Password reset email sent!",
55
+ "login.setPassword": "Set Password",
56
+ "login.signIn": "Sign In",
57
+ "login.signInTitle": "Please Sign In",
58
+ "login.verifyPassword": "Verify Password",
59
+ "login.welcomeName": "Welcome back, {}!",
60
+ "login.welcomeBack": "Welcome back",
61
+
62
+ // Login validation
63
+ "login.validate.email": "Please enter a valid email address.",
64
+ "login.validate.firstName": "Please enter your first name.",
65
+ "login.validate.invalid": "Invalid login. Please check your email or password.",
66
+ "login.validate.lastName": "Please enter your last name.",
67
+ "login.validate.password": "Please enter a password.",
68
+ "login.validate.passwordLength": "Password must be at least 8 characters long.",
69
+ "login.validate.passwordMatch": "Passwords do not match.",
70
+ "login.validate.selectingChurch": "Please select a church.",
71
+
72
+ // Church selection
73
+ "selectChurch.address1": "Address Line 1",
74
+ "selectChurch.address2": "Address Line 2",
75
+ "selectChurch.another": "Choose another church",
76
+ "selectChurch.city": "City",
77
+ "selectChurch.confirmRegister": "Are you sure you wish to register a new church?",
78
+ "selectChurch.country": "Country",
79
+ "selectChurch.name": "Church Name",
80
+ "selectChurch.noMatches": "No matches found.",
81
+ "selectChurch.register": "Register a New Church",
82
+ "selectChurch.selectChurch": "Select a Church",
83
+ "selectChurch.state": "State / Province",
84
+ "selectChurch.zip": "Zip / Postal Code",
85
+
86
+ // Church selection validation
87
+ "selectChurch.validate.address": "Address cannot be blank.",
88
+ "selectChurch.validate.city": "City cannot be blank.",
89
+ "selectChurch.validate.country": "Country cannot be blank.",
90
+ "selectChurch.validate.name": "Church name cannot be blank.",
91
+ "selectChurch.validate.state": "State/Province cannot be blank.",
92
+ "selectChurch.validate.zip": "Zip/Postal code cannot be blank."
93
+ };
94
+
95
+ static init = async (backends: string[]): Promise<void> => {
96
+ const resources: TranslationResources = {};
97
+ let langs = ["en"];
98
+
99
+ if (typeof navigator !== "undefined") {
100
+ const browserLang = navigator.language.split("-")[0];
101
+ const mappedLang
102
+ = Object.keys(this.extraCodes).find((code) =>
103
+ this.extraCodes[code].includes(browserLang),
104
+ ) || browserLang;
105
+ const notSupported = this.supportedLanguages.indexOf(mappedLang) === -1;
106
+ langs = mappedLang === "en" || notSupported ? ["en"] : ["en", mappedLang];
107
+ }
108
+
109
+ // Initialize resources with fallbacks
110
+ resources["en"] = { translation: this.fallbacks };
111
+
112
+ // Load translations for each language
113
+ for (const lang of langs) {
114
+ if (!resources[lang]) {
115
+ resources[lang] = { translation: {} };
116
+ }
117
+
118
+ try {
119
+ for (const backend of backends) {
120
+ const url = backend.replace("{{lng}}", lang);
121
+ try {
122
+ const response = await fetch(url);
123
+ if (response.ok) {
124
+ const data = await response.json();
125
+ resources[lang].translation = this.deepMerge(
126
+ resources[lang].translation,
127
+ data,
128
+ );
129
+ }
130
+ } catch (error) {
131
+ console.warn(`Failed to load translations from ${url}:`, error);
132
+ }
133
+ }
134
+ } catch (error) {
135
+ console.warn(`Failed to load translations for language ${lang}:`, error);
136
+ }
137
+ }
138
+
139
+ // Initialize i18n
140
+ try {
141
+ await i18n
142
+ .use(Backend)
143
+ .use(LanguageDetector)
144
+ .use(initReactI18next)
145
+ .init({
146
+ resources,
147
+ fallbackLng: "en",
148
+ debug: false,
149
+ interpolation: {
150
+ escapeValue: false,
151
+ },
152
+ detection: {
153
+ order: ["navigator"],
154
+ caches: ["localStorage"],
155
+ },
156
+ });
157
+ } catch (error) {
158
+ console.warn("Failed to initialize i18n:", error);
159
+ }
160
+ };
161
+
162
+ private static deepMerge(
163
+ target: Record<string, unknown>,
164
+ source: Record<string, unknown>,
165
+ ): Record<string, unknown> {
166
+ for (const key in source) {
167
+ if (this.isObject(source[key])) {
168
+ if (!target[key]) Object.assign(target, { [key]: {} });
169
+ this.deepMerge(
170
+ target[key] as Record<string, unknown>,
171
+ source[key] as Record<string, unknown>,
172
+ );
173
+ } else Object.assign(target, { [key]: source[key] });
174
+ }
175
+ return target;
176
+ }
177
+
178
+ private static isObject(obj: unknown): boolean {
179
+ return obj !== null && typeof obj === "object" && !Array.isArray(obj);
180
+ }
181
+
182
+ // New helper method that uses i18n with fallback
183
+ static t(key: string, options?: Record<string, unknown>): string {
184
+ try {
185
+ // Check if i18n is initialized and has the key
186
+ if (i18n && i18n.isInitialized && i18n.exists(key)) {
187
+ const translation = i18n.t(key, options);
188
+ // If translation is not just the key (which indicates missing translation)
189
+ if (translation !== key) {
190
+ return translation;
191
+ }
192
+ }
193
+ } catch (error) {
194
+ console.warn(`Error getting translation for key "${key}":`, error);
195
+ }
196
+
197
+ // Fall back to our local fallbacks
198
+ const fallback = this.fallbacks[key];
199
+ if (fallback) {
200
+ // Handle simple string replacement like {} placeholders
201
+ if (options && typeof options === 'object') {
202
+ let result = fallback;
203
+ Object.entries(options).forEach(([placeholder, value]) => {
204
+ if (typeof value === 'string' || typeof value === 'number') {
205
+ result = result.replace(`{${placeholder}}`, String(value));
206
+ result = result.replace('{}', String(value)); // Handle unnamed placeholders
207
+ }
208
+ });
209
+ return result;
210
+ }
211
+ return fallback;
212
+ }
213
+
214
+ // Ultimate fallback - return the key itself with a warning
215
+ console.warn(`No translation found for key: ${key}`);
216
+ return key;
217
+ }
218
+
219
+ // Keep the old method for backward compatibility
220
+ static label(key: string): string {
221
+ return this.t(key);
222
+ }
223
+
224
+ // Helper method to check if translations are available
225
+ static isInitialized(): boolean {
226
+ return i18n && i18n.isInitialized;
227
+ }
228
+
229
+ // Method to set up basic fallback-only mode (no i18n)
230
+ static initFallbackMode(): void {
231
+ console.info("Locale: Running in fallback mode with English labels only");
232
+ }
233
+ }
@@ -0,0 +1,2 @@
1
+ export { Locale } from "./Locale";
2
+ export { AnalyticsHelper } from "./AnalyticsHelper";
package/src/index.ts ADDED
@@ -0,0 +1,10 @@
1
+ export { LoginPage } from "./LoginPage";
2
+ export { LogoutPage } from "./LogoutPage";
3
+ export { Register } from "./components/Register";
4
+ export { Login } from "./components/Login";
5
+ export { Forgot } from "./components/Forgot";
6
+ export { LoginSetPassword } from "./components/LoginSetPassword";
7
+ export { SelectChurchModal } from "./components/SelectChurchModal";
8
+ export { SelectChurchRegister } from "./components/SelectChurchRegister";
9
+ export { SelectChurchSearch } from "./components/SelectChurchSearch";
10
+ export { SelectableChurch } from "./components/SelectableChurch";
package/tsconfig.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "compilerOptions": {
3
+ "module": "ESNext",
4
+ "esModuleInterop": true,
5
+ "lib": ["ES2021", "DOM"],
6
+ "typeRoots": ["node_modules/@types"],
7
+ "target": "ES2020",
8
+ "noImplicitAny": true,
9
+ "moduleResolution": "node",
10
+ "sourceMap": true,
11
+ "outDir": "dist",
12
+ "baseUrl": ".",
13
+ "strict": false,
14
+ "allowSyntheticDefaultImports": true,
15
+ "declaration": true,
16
+ "declarationMap": true,
17
+ "jsx": "react-jsx",
18
+ "skipLibCheck": true,
19
+ "forceConsistentCasingInFileNames": true,
20
+ "resolveJsonModule": true,
21
+ "isolatedModules": true
22
+ },
23
+ "include": [
24
+ "src/**/*"
25
+ ],
26
+ "exclude": [
27
+ "node_modules",
28
+ "dist"
29
+ ]
30
+ }