Rebrand to Waseel, swap Stripe for Areeba, add phone-ready auth and DB seed
- Rename app: Waseel (name, slug, scheme waseel://, com.waseel.app ids, splash) - Payments: replace Stripe with Areeba hosted checkout (create/verify API routes, lib/areeba.ts, WebBrowser-based payment flow) - Maps: migrate address autocomplete to Places API (New), drop legacy library - Web support: map stub for web (native maps are iOS/Android only) - Auth: keep email + Google OAuth; fix OAuth redirect for Expo Go - Add scripts/seed-db.mjs (schema + Lebanese driver seed) - Pin Expo SDK 51 compatible package versions
This commit is contained in:
+115
@@ -0,0 +1,115 @@
|
||||
// Server-side helpers for the Areeba payment gateway (MPGS-style API).
|
||||
//
|
||||
// Areeba issues merchant credentials after onboarding:
|
||||
// AREEBA_API_BASE_URL e.g. https://<your-gateway-host>.areeba.com
|
||||
// AREEBA_MERCHANT_ID your merchant id
|
||||
// AREEBA_API_PASSWORD API password for the merchant
|
||||
// AREEBA_API_VERSION gateway REST API version (default: 100)
|
||||
//
|
||||
// NOTE: verify the exact field names against the integration docs Areeba
|
||||
// sends you — the gateway is Mastercard Payment Gateway (MPGS) based, and
|
||||
// the payloads below follow that convention.
|
||||
|
||||
export const areebaConfig = () => {
|
||||
const baseUrl = process.env.AREEBA_API_BASE_URL?.replace(/\/$/, "");
|
||||
const merchantId = process.env.AREEBA_MERCHANT_ID;
|
||||
const apiPassword = process.env.AREEBA_API_PASSWORD;
|
||||
const apiVersion = process.env.AREEBA_API_VERSION || "100";
|
||||
|
||||
if (!baseUrl || !merchantId || !apiPassword) {
|
||||
throw new Error(
|
||||
"Missing Areeba configuration. Set AREEBA_API_BASE_URL, AREEBA_MERCHANT_ID and AREEBA_API_PASSWORD in .env",
|
||||
);
|
||||
}
|
||||
|
||||
return { baseUrl, merchantId, apiPassword, apiVersion };
|
||||
};
|
||||
|
||||
const authHeader = (merchantId: string, apiPassword: string) =>
|
||||
`Basic ${Buffer.from(`merchant.${merchantId}:${apiPassword}`).toString("base64")}`;
|
||||
|
||||
// Creates a checkout session and returns the hosted payment page URL.
|
||||
export const createCheckoutSession = async ({
|
||||
orderId,
|
||||
amount,
|
||||
currency,
|
||||
description,
|
||||
returnUrl,
|
||||
}: {
|
||||
orderId: string;
|
||||
amount: number; // major units, e.g. 25.5 USD
|
||||
currency: string;
|
||||
description: string;
|
||||
returnUrl: string;
|
||||
}) => {
|
||||
const { baseUrl, merchantId, apiPassword, apiVersion } = areebaConfig();
|
||||
|
||||
const res = await fetch(
|
||||
`${baseUrl}/api/rest/version/${apiVersion}/merchant/${merchantId}/session`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: authHeader(merchantId, apiPassword),
|
||||
},
|
||||
body: JSON.stringify({
|
||||
apiOperation: "INITIATE_CHECKOUT",
|
||||
order: {
|
||||
id: orderId,
|
||||
amount: amount.toFixed(2),
|
||||
currency,
|
||||
description,
|
||||
},
|
||||
interaction: {
|
||||
operation: "PURCHASE",
|
||||
merchant: { name: "Waseel" },
|
||||
returnUrl,
|
||||
},
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (!res.ok || !data?.session?.id) {
|
||||
console.log("[AREEBA_CREATE_SESSION]: ", data);
|
||||
throw new Error(
|
||||
data?.error?.explanation || "Failed to create Areeba checkout session",
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
sessionId: data.session.id as string,
|
||||
// Used to verify the redirect result (compare with resultIndicator).
|
||||
successIndicator: data.successIndicator as string | undefined,
|
||||
checkoutUrl: `${baseUrl}/checkout/pay/${data.session.id}?checkoutVersion=1.0.0`,
|
||||
};
|
||||
};
|
||||
|
||||
// Retrieves an order and reports whether it was paid.
|
||||
export const retrieveOrder = async (orderId: string) => {
|
||||
const { baseUrl, merchantId, apiPassword, apiVersion } = areebaConfig();
|
||||
|
||||
const res = await fetch(
|
||||
`${baseUrl}/api/rest/version/${apiVersion}/merchant/${merchantId}/order/${orderId}`,
|
||||
{ headers: { Authorization: authHeader(merchantId, apiPassword) } },
|
||||
);
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (!res.ok) {
|
||||
console.log("[AREEBA_RETRIEVE_ORDER]: ", data);
|
||||
throw new Error(
|
||||
data?.error?.explanation || "Failed to retrieve Areeba order",
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
status: data.status as string | undefined,
|
||||
result: data.result as string | undefined,
|
||||
amount: data.amount as string | undefined,
|
||||
currency: data.currency as string | undefined,
|
||||
// PURCHASE auto-captures; CAPTURED means the money was taken.
|
||||
paid: data.result === "SUCCESS" && data.status === "CAPTURED",
|
||||
};
|
||||
};
|
||||
+4
-3
@@ -44,10 +44,11 @@ type StartOAuthFlowType = (
|
||||
|
||||
export const googleOAuth = async (startOAuthFlow: StartOAuthFlowType) => {
|
||||
try {
|
||||
// No explicit scheme: in Expo Go this resolves to exp://... so the
|
||||
// browser can redirect back into the app; in a standalone build it
|
||||
// automatically uses the app.json scheme ("waseel").
|
||||
const { createdSessionId, signUp, setActive } = await startOAuthFlow({
|
||||
redirectUrl: Linking.createURL("/(root)/(tabs)/home", {
|
||||
scheme: "ryde", // match with scheme in app.json
|
||||
}),
|
||||
redirectUrl: Linking.createURL("/(root)/(tabs)/home"),
|
||||
});
|
||||
|
||||
if (createdSessionId) {
|
||||
|
||||
@@ -44,3 +44,12 @@ export function formatDate(dateString: string): string {
|
||||
|
||||
return `${day < 10 ? "0" + day : day} ${month} ${year}`;
|
||||
}
|
||||
|
||||
// Normalizes a Lebanese phone number to E.164 (+961...).
|
||||
export function normalizePhone(raw: string): string {
|
||||
const cleaned = raw.replace(/[^\d+]/g, "");
|
||||
|
||||
if (cleaned.startsWith("+")) return cleaned;
|
||||
|
||||
return `+961${cleaned.replace(/^0+/, "")}`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user