Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,13 @@ MINISTRY_PLATFORM_DEV_CLIENT_SECRET=
# Enables address autocomplete on the Address Line 1 field in the Add/Edit
# Family tool (and any other tool that uses GooglePlacesService).
#
# When unset, the tool falls back to a plain text input — no errors, no UI
# change beyond losing the suggestion dropdown.
# Key resolution order (first non-empty wins):
# 1. MinistryPlatform dp_Configuration_Settings
# (Application_Code='COMMON', Key_Name='GoogleMapsAPIKey')
# 2. This GOOGLE_PLACES_API_KEY environment variable
# 3. If neither is set, the feature is disabled — the tool falls back to
# a plain text input with no errors and no UI change beyond losing the
# suggestion dropdown.
#
# Get a key at https://console.cloud.google.com and enable the "Places API
# (New)" — the v1 REST endpoints. Calls go through our server actions, so
Expand Down
2 changes: 1 addition & 1 deletion src/app/(web)/tools/addeditfamily/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ export async function placeAutocomplete(
await getSession();
if (input.trim().length < 3) return [];
const service = await GooglePlacesService.getInstance();
if (!service.isEnabled()) return [];
if (!(await service.isEnabled())) return [];
return service.autocomplete(input, sessionToken);
}

Expand Down
53 changes: 46 additions & 7 deletions src/services/googlePlacesService.ts
Original file line number Diff line number Diff line change
@@ -1,40 +1,79 @@
import { MPHelper } from "@/lib/providers/ministry-platform";
import { GooglePlacesProvider } from "@/lib/providers/google-places";
import type { PlacePrediction, PlaceDetails } from "@/lib/providers/google-places";

export class GooglePlacesService {
private static instance: GooglePlacesService;
private mp: MPHelper | null = null;
private provider: GooglePlacesProvider | null = null;
// undefined = not yet resolved; null = resolved with no key (feature disabled)
private resolvedKey: string | null | undefined = undefined;

private constructor() {}

public static async getInstance(): Promise<GooglePlacesService> {
if (!GooglePlacesService.instance) {
GooglePlacesService.instance = new GooglePlacesService();
GooglePlacesService.instance.mp = new MPHelper();
}
return GooglePlacesService.instance;
}

public isEnabled(): boolean {
return Boolean(process.env.GOOGLE_PLACES_API_KEY);
/**
* Resolves the Google Places API key with this precedence:
* 1. MP dp_Configuration_Settings (Application_Code='COMMON', Key_Name='GoogleMapsAPIKey')
* 2. GOOGLE_PLACES_API_KEY environment variable
* 3. null (feature disabled)
*
* Cached on the singleton so MP is queried at most once per process lifetime.
*/
private async resolveApiKey(): Promise<string | null> {
if (this.resolvedKey !== undefined) return this.resolvedKey;

try {
const rows = await this.mp!.getTableRecords<{ Value: string | null }>({
table: "dp_Configuration_Settings",
select: "Value",
filter: "Application_Code='COMMON' AND Key_Name='GoogleMapsAPIKey'",
top: 1,
});
const mpKey = rows[0]?.Value?.trim();
if (mpKey) {
this.resolvedKey = mpKey;
return this.resolvedKey;
}
} catch {
// Swallow lookup failures (e.g. table permissions) and fall through to env var.
}

const envKey = process.env.GOOGLE_PLACES_API_KEY?.trim();
this.resolvedKey = envKey ? envKey : null;
return this.resolvedKey;
}

public async isEnabled(): Promise<boolean> {
return (await this.resolveApiKey()) !== null;
}

private getProvider(): GooglePlacesProvider {
private async getProvider(): Promise<GooglePlacesProvider> {
if (this.provider) return this.provider;
const apiKey = process.env.GOOGLE_PLACES_API_KEY;
const apiKey = await this.resolveApiKey();
if (!apiKey) {
throw new Error(
"GOOGLE_PLACES_API_KEY is not configured. Add it to .env.local to enable address autocomplete.",
"Google Places API key is not configured. Set the 'GoogleMapsAPIKey' setting in MinistryPlatform (Application_Code='COMMON') or define GOOGLE_PLACES_API_KEY in .env.local.",
);
}
this.provider = new GooglePlacesProvider(apiKey);
return this.provider;
}

async autocomplete(input: string, sessionToken: string): Promise<PlacePrediction[]> {
return this.getProvider().autocomplete(input, sessionToken);
const provider = await this.getProvider();
return provider.autocomplete(input, sessionToken);
}

async getPlaceDetails(placeId: string, sessionToken: string): Promise<PlaceDetails> {
return this.getProvider().getPlaceDetails(placeId, sessionToken);
const provider = await this.getProvider();
return provider.getPlaceDetails(placeId, sessionToken);
}
}
Loading