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
8 changes: 6 additions & 2 deletions docs/install.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,18 @@ sidebar_position: 1
description: 'Install `@react-native-google-signin/google-signin`. Covers paid (Universal Sign In) and free (Original) versions, requirements, and package manager setup.'
---

# Installation
import CreemEmbeddedCheckout from '@site/src/components/CreemEmbeddedCheckout';

> We recommend [Expo](https://expo.dev) and [EAS](https://expo.dev/eas) for building and deploying your React Native app. Expo offers the best developer experience and is well-supported by this library.
# Installation

The recommended option is [Universal Sign In](https://universal-sign-in.com) (paid). A free legacy version is also available — see [below](#public-version-free) for the differences. If you are an EAS customer, you may be able to access the paid version for free, [learn more](https://forms.gle/tpP7TfUGW1CwgaEZ8).

Why paid? According to the [State of React Native Survey](https://results.2024.stateofreactnative.com/en-US/opinions/#opinions_pain_points_multiple), unmaintained packages are **the #1 pain point** of the React Native ecosystem. Your purchase enables the module to be rock-solid, and contributions to upstream SDKs such as [1](https://github.com/openid/AppAuth-iOS/pull/788), [2](https://github.com/google/GoogleSignIn-iOS/pull/402), [3](https://github.com/googlesamples/google-services/issues/426), [4](https://github.com/google/GoogleSignIn-iOS/issues/457), [5](https://issuetracker.google.com/issues/424210681), [6](https://issuetracker.google.com/issues/474817166).

[//]: # '🌟'

<CreemEmbeddedCheckout checkoutUrl="https://www.creem.io/checkout/prod_2zElFBVDXGigzZTiK4MvYX/ch_5PmaTQdtYUlCLt08HF0bkE" />

### Universal Sign In (premium) {/* #premium */}

⭐️ **Key Features**:
Expand Down Expand Up @@ -166,3 +168,5 @@ The latest version of the Universal Sign In package supports (use older versions
| ------------ | --------------- |
| expo | 52.0.40 - 56 |
| react-native | 0.76.0 - 0.86 |

> We recommend [Expo](https://expo.dev) and [EAS](https://expo.dev/eas) for building and deploying your React Native app. Expo offers the best developer experience and is well-supported by this library.
191 changes: 191 additions & 0 deletions src/components/CreemEmbeddedCheckout/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
import React, { useEffect, useId, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import './styles.css';

type CreemEmbeddedCheckoutProps = {
checkoutUrl: string;
};

const CREEM_ORIGIN = 'https://www.creem.io';

export default function CreemEmbeddedCheckout({
checkoutUrl,
}: CreemEmbeddedCheckoutProps) {
const [isOpen, setIsOpen] = useState(false);
const [shouldLoadCheckout, setShouldLoadCheckout] = useState(false);
const titleId = useId();
const closeButtonRef = useRef<HTMLButtonElement>(null);
const iframeRef = useRef<HTMLIFrameElement>(null);
const modalContentRef = useRef<HTMLDivElement>(null);

const loadCheckout = () => {
setShouldLoadCheckout(true);
};

const openCheckout = () => {
loadCheckout();
setIsOpen(true);
};

useEffect(() => {
if (document.querySelector(`link[rel="preconnect"][href="${CREEM_ORIGIN}"]`)) {
return;
}

const link = document.createElement('link');
link.href = CREEM_ORIGIN;
link.rel = 'preconnect';
link.crossOrigin = 'anonymous';
document.head.appendChild(link);
}, []);

useEffect(() => {
if (!isOpen) {
return;
}

const previousOverflow = document.body.style.overflow;
const previousActiveElement = document.activeElement;
const getFocusableElements = () =>
Array.from(
modalContentRef.current?.querySelectorAll<HTMLElement>(
'button, iframe, [data-focus-sentinel]',
) ?? [],
).filter(
(element) =>
!element.hasAttribute('disabled') &&
element.getAttribute('aria-hidden') !== 'true',
);

const onKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
setIsOpen(false);
return;
}

if (event.key !== 'Tab') {
return;
}

if (
event.shiftKey &&
document.activeElement === closeButtonRef.current
) {
event.preventDefault();
iframeRef.current?.focus();
return;
}

const focusableElements = getFocusableElements();
const firstElement = focusableElements[0];
const lastElement = focusableElements[focusableElements.length - 1];

if (!firstElement || !lastElement) {
event.preventDefault();
return;
}

if (event.shiftKey && document.activeElement === firstElement) {
event.preventDefault();
lastElement.focus();
return;
}

if (!event.shiftKey && document.activeElement === lastElement) {
event.preventDefault();
firstElement.focus();
}
};

document.addEventListener('keydown', onKeyDown);
document.body.style.overflow = 'hidden';
closeButtonRef.current?.focus();

return () => {
document.removeEventListener('keydown', onKeyDown);
document.body.style.overflow = previousOverflow;

if (previousActiveElement instanceof HTMLElement) {
previousActiveElement.focus();
}
};
}, [isOpen]);

return (
<section className="creem-embed" aria-labelledby="creem-embed-title">
<div className="creem-embed__header">
<div>
<p className="creem-embed__eyebrow">Universal Sign In license</p>
<h2 id="creem-embed-title">Ready to install Universal Sign In?</h2>
<p>
Buy a license, then use the private npm registry setup below to add
the package to your app.
</p>
</div>
<button
className="button button--primary creem-embed__button"
type="button"
onClick={openCheckout}
onFocus={loadCheckout}
onPointerEnter={loadCheckout}
>
Buy license
</button>
</div>
{shouldLoadCheckout &&
createPortal(
<div
className={`creem-embed-modal ${
isOpen ? '' : 'creem-embed-modal--preload'
}`}
role={isOpen ? 'dialog' : undefined}
aria-modal={isOpen ? 'true' : undefined}
aria-labelledby={isOpen ? titleId : undefined}
aria-hidden={!isOpen}
>
<button
className="creem-embed-modal__backdrop"
type="button"
aria-label="Close embedded checkout"
onClick={() => setIsOpen(false)}
/>
<div className="creem-embed-modal__content" ref={modalContentRef}>
<div className="creem-embed-modal__header">
<div>
<p className="creem-embed-modal__eyebrow">Secure checkout</p>
<h2 id={titleId}>Complete your purchase</h2>
</div>
<button
className="creem-embed-modal__close"
ref={closeButtonRef}
type="button"
aria-label="Close embedded checkout"
onClick={() => setIsOpen(false)}
/>
</div>
<span
className="creem-embed-modal__sentinel"
data-focus-sentinel
tabIndex={0}
onFocus={() => iframeRef.current?.focus()}
/>
<iframe
className="creem-embed-modal__frame"
ref={iframeRef}
src={checkoutUrl}
title="Universal Sign In checkout"
allow="payment *; publickey-credentials-get *"
/>
<span
className="creem-embed-modal__sentinel"
data-focus-sentinel
tabIndex={0}
onFocus={() => closeButtonRef.current?.focus()}
/>
</div>
</div>,
document.body,
)}
</section>
);
}
163 changes: 163 additions & 0 deletions src/components/CreemEmbeddedCheckout/styles.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
.creem-embed {
margin: 2rem 0;
padding: 1rem;
border: 1px solid var(--ifm-color-emphasis-300);
border-radius: 8px;
background: var(--ifm-background-surface-color);
}

.creem-embed__header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 1rem;
}

.creem-embed__button {
flex-shrink: 0;
}

.creem-embed__eyebrow,
.creem-embed-modal__eyebrow {
margin: 0 0 0.25rem;
color: var(--ifm-color-primary);
font-size: 0.78rem;
font-weight: 700;
letter-spacing: 0.04em;
text-transform: uppercase;
}

.creem-embed__header h2 {
max-width: 32rem;
margin: 0 0 0.5rem;
}

.creem-embed__header p {
margin: 0;
}

.creem-embed-modal {
position: fixed;
inset: 0;
z-index: 9999;
display: grid;
place-items: center;
padding: 1rem 0;
}

.creem-embed-modal--preload {
visibility: hidden;
opacity: 0;
pointer-events: none;
}

.creem-embed-modal__backdrop {
position: absolute;
inset: 0;
border: 0;
background: rgba(15, 23, 42, 0.68);
cursor: pointer;
}

.creem-embed-modal__content {
position: relative;
z-index: 1;
display: flex;
flex-direction: column;
width: min(100%, 580px);
max-height: min(900px, calc(100vh - 2rem));
overflow: hidden;
border-radius: 8px;
background: var(--ifm-background-surface-color);
box-shadow: 0 24px 80px rgba(15, 23, 42, 0.35);
}

.creem-embed-modal__header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding: 0.75rem 1rem;
border-bottom: 1px solid var(--ifm-color-emphasis-200);
}

.creem-embed-modal__header h2 {
margin: 0;
font-size: 1rem;
}

.creem-embed-modal__close {
position: relative;
flex-shrink: 0;
width: 2.25rem;
height: 2.25rem;
border: 1px solid var(--ifm-color-emphasis-300);
border-radius: 999px;
background: var(--ifm-background-surface-color);
color: var(--ifm-font-color-base);
cursor: pointer;
transition:
background 160ms ease,
border-color 160ms ease,
box-shadow 160ms ease,
transform 160ms ease;
}

.creem-embed-modal__close::before,
.creem-embed-modal__close::after {
position: absolute;
top: 50%;
left: 50%;
width: 0.9rem;
height: 2px;
border-radius: 999px;
background: currentColor;
content: '';
}

.creem-embed-modal__close::before {
transform: translate(-50%, -50%) rotate(45deg);
}

.creem-embed-modal__close::after {
transform: translate(-50%, -50%) rotate(-45deg);
}

.creem-embed-modal__close:hover,
.creem-embed-modal__close:focus-visible {
border-color: var(--ifm-color-primary);
background: var(--ifm-color-emphasis-100);
box-shadow: 0 0 0 3px rgba(26, 115, 232, 0.18);
outline: none;
}

.creem-embed-modal__close:hover {
transform: translateY(-1px);
}

.creem-embed-modal__frame {
align-self: center;
width: 100%;
height: min(820px, calc(100vh - 6.5rem));
border: 0;
background: #ffffff;
}

.creem-embed-modal__sentinel {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip: rect(0 0 0 0);
white-space: nowrap;
}

@media (max-width: 700px) {
.creem-embed__header {
flex-direction: column;
}

.creem-embed__button {
width: 100%;
}
}