> ## Documentation Index
> Fetch the complete documentation index at: https://docs.corbado.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Passkey Creation: On 2nd Device

> Passkey creation after fallback when the account's existing passkeys were created before the current device was first seen.

export const ToolingFrame = ({imageURL, caption, imageName, containerBackground = 'rgb(249,250,252,1)'}) => {
  React.useEffect(() => {
    const style = document.createElement('style');
    style.textContent = `
            .drag-hint {
                bottom: 20px;
            }
            .caption-text {
                font-size: 16px;
            }
            @media (max-width: 768px) {
                .drag-hint {
                    bottom: 80px !important;
                }
                .caption-text {
                    font-size: 12px !important;
                }
                .caption-container {
                    padding: 6px !important;
                }
            }
        `;
    document.head.appendChild(style);
    return () => document.head.removeChild(style);
  }, []);
  const [showLightbox, setShowLightbox] = React.useState(false);
  const [viewportPadding, setViewportPadding] = React.useState(100);
  const imgRef = React.useRef(null);
  const containerRef = React.useRef(null);
  const imageTransformRef = React.useRef(null);
  const scaleDisplayRef = React.useRef(null);
  const dragHintRef = React.useRef(null);
  const zoomInBtnRef = React.useRef(null);
  const zoomOutBtnRef = React.useRef(null);
  const resetBtnRef = React.useRef(null);
  const stateRef = React.useRef({
    isDragging: false,
    isZooming: false,
    position: {
      x: 0,
      y: 0
    },
    dragStart: {
      x: 0,
      y: 0
    },
    initialDistance: 0,
    imageDimensions: {
      width: 0,
      height: 0
    },
    imageLoaded: false,
    naturalDimensions: {
      width: 0,
      height: 0
    },
    initialDimensions: {
      width: 0,
      height: 0
    },
    gestureBaseWidth: 0
  });
  const maxScale = 3;
  const zoomStep = 0.2;
  const getViewportPadding = React.useCallback(() => {
    const width = typeof window !== 'undefined' ? window.innerWidth : 1200;
    if (width < 640) return 20;
    if (width < 1024) return 40;
    return 100;
  }, []);
  React.useEffect(() => {
    const updatePadding = () => setViewportPadding(getViewportPadding());
    updatePadding();
    window.addEventListener('resize', updatePadding);
    return () => window.removeEventListener('resize', updatePadding);
  }, [getViewportPadding]);
  const updateTransform = () => {
    if (imageTransformRef.current) {
      const {position, imageDimensions} = stateRef.current;
      imageTransformRef.current.style.transform = `translate(${position.x}px, ${position.y}px)`;
      const img = imageTransformRef.current.querySelector('img');
      if (img && imageDimensions.width) {
        img.style.width = `${imageDimensions.width}px`;
        img.style.height = 'auto';
        void img.offsetHeight;
        const actualHeight = img.offsetHeight;
        if (actualHeight && actualHeight !== imageDimensions.height) {
          stateRef.current.imageDimensions.height = actualHeight;
        }
      }
    }
    if (scaleDisplayRef.current) {
      const {imageDimensions, initialDimensions} = stateRef.current;
      if (initialDimensions.width) {
        const relativeScale = imageDimensions.width / initialDimensions.width * 100;
        scaleDisplayRef.current.textContent = `${Math.round(relativeScale)}%`;
      }
    }
    updateButtonStates();
    updateDragHint();
  };
  const updateButtonStates = () => {
    const {imageDimensions, position, initialDimensions} = stateRef.current;
    if (zoomInBtnRef.current && initialDimensions.width) {
      const absoluteMaxWidth = initialDimensions.width * maxScale;
      const isDisabled = imageDimensions.width >= absoluteMaxWidth;
      zoomInBtnRef.current.style.opacity = isDisabled ? '0.5' : '1';
      zoomInBtnRef.current.style.cursor = isDisabled ? 'not-allowed' : 'pointer';
    }
    if (zoomOutBtnRef.current && initialDimensions.width) {
      const isDisabled = imageDimensions.width <= initialDimensions.width;
      zoomOutBtnRef.current.style.opacity = isDisabled ? '0.5' : '1';
      zoomOutBtnRef.current.style.cursor = isDisabled ? 'not-allowed' : 'pointer';
    }
    if (resetBtnRef.current && initialDimensions.width) {
      const isDisabled = imageDimensions.width === initialDimensions.width && position.x === 0 && position.y === 0;
      resetBtnRef.current.style.opacity = isDisabled ? '0.5' : '1';
      resetBtnRef.current.style.cursor = isDisabled ? 'not-allowed' : 'pointer';
    }
  };
  const updateDragHint = () => {
    if (dragHintRef.current && containerRef.current) {
      const {imageDimensions} = stateRef.current;
      const imgWidth = imageDimensions.width;
      const imgHeight = imageDimensions.height;
      const containerWidth = containerRef.current.offsetWidth;
      const containerHeight = containerRef.current.offsetHeight;
      const shouldShow = imgWidth > containerWidth || imgHeight > containerHeight;
      dragHintRef.current.style.display = shouldShow ? 'block' : 'none';
    }
  };
  const updateContainerCursor = () => {
    if (!containerRef.current) return;
    const {isDragging, imageLoaded, imageDimensions} = stateRef.current;
    if (!imageLoaded) {
      containerRef.current.style.cursor = 'wait';
      return;
    }
    if (isDragging) {
      containerRef.current.style.cursor = 'grabbing';
      return;
    }
    const imgWidth = imageDimensions.width;
    const imgHeight = imageDimensions.height;
    const containerWidth = containerRef.current.offsetWidth;
    const containerHeight = containerRef.current.offsetHeight;
    const isOverflowing = imgWidth > containerWidth || imgHeight > containerHeight;
    containerRef.current.style.cursor = isOverflowing ? 'grab' : 'default';
  };
  const handleDownload = async e => {
    e.preventDefault();
    e.stopPropagation();
    try {
      const response = await fetch(imageURL);
      const blob = await response.blob();
      const url = window.URL.createObjectURL(blob);
      const link = document.createElement('a');
      link.href = url;
      link.download = imageName || 'image.webp';
      document.body.appendChild(link);
      link.click();
      document.body.removeChild(link);
      window.URL.revokeObjectURL(url);
    } catch (error) {
      console.error('Download failed:', error);
    }
  };
  const handleImageClick = e => {
    e.preventDefault();
    e.stopPropagation();
    setShowLightbox(true);
  };
  const closeLightbox = () => {
    setShowLightbox(false);
    stateRef.current = {
      isDragging: false,
      isZooming: false,
      position: {
        x: 0,
        y: 0
      },
      dragStart: {
        x: 0,
        y: 0
      },
      initialDistance: 0,
      imageDimensions: {
        width: 0,
        height: 0
      },
      imageLoaded: false,
      naturalDimensions: {
        width: 0,
        height: 0
      },
      initialDimensions: {
        width: 0,
        height: 0
      },
      gestureBaseWidth: 0
    };
  };
  const handleMouseDown = e => {
    const state = stateRef.current;
    if (!containerRef.current || !state.imageDimensions.width || !state.imageDimensions.height) return;
    const containerWidth = containerRef.current.offsetWidth;
    const containerHeight = containerRef.current.offsetHeight;
    const imgWidth = state.imageDimensions.width;
    const imgHeight = state.imageDimensions.height;
    const isOverflowing = imgWidth - containerWidth >= 1 || imgHeight - containerHeight >= 1;
    if (!isOverflowing) return;
    state.isDragging = true;
    state.dragStart = {
      x: e.clientX - state.position.x,
      y: e.clientY - state.position.y
    };
    updateContainerCursor();
  };
  const handleMouseMove = e => {
    const state = stateRef.current;
    if (!state.isDragging || !containerRef.current) return;
    let newX = e.clientX - state.dragStart.x;
    let newY = e.clientY - state.dragStart.y;
    if (state.imageDimensions.width && state.imageDimensions.height) {
      const containerWidth = containerRef.current.offsetWidth;
      const containerHeight = containerRef.current.offsetHeight;
      const imgWidth = state.imageDimensions.width;
      const imgHeight = state.imageDimensions.height;
      const isWider = imgWidth - containerWidth >= 1;
      const isTaller = imgHeight - containerHeight >= 1;
      if (isWider) {
        const halfOverflowX = (imgWidth - containerWidth) / 2;
        newX = Math.min(Math.max(newX, -halfOverflowX), halfOverflowX);
      } else {
        newX = 0;
      }
      if (isTaller) {
        const halfOverflowY = (imgHeight - containerHeight) / 2;
        newY = Math.min(Math.max(newY, -halfOverflowY), halfOverflowY);
      } else {
        newY = 0;
      }
    }
    state.position = {
      x: newX,
      y: newY
    };
    updateTransform();
  };
  const handleMouseUp = () => {
    stateRef.current.isDragging = false;
    updateContainerCursor();
  };
  const handleZoom = (newWidth, centerX, centerY) => {
    const state = stateRef.current;
    if (!containerRef.current || !state.imageDimensions.width || !state.imageDimensions.height) return;
    if (!state.initialDimensions.width || !state.initialDimensions.height) return;
    if (!state.naturalDimensions.width || !state.naturalDimensions.height) return;
    const absoluteMaxWidth = state.initialDimensions.width * maxScale;
    const boundedWidth = Math.max(state.initialDimensions.width, Math.min(absoluteMaxWidth, newWidth));
    const aspectRatio = state.naturalDimensions.height / state.naturalDimensions.width;
    const boundedHeight = boundedWidth * aspectRatio;
    let newX = state.position.x;
    let newY = state.position.y;
    if (centerX !== undefined && centerY !== undefined) {
      const rect = containerRef.current.getBoundingClientRect();
      const offsetX = centerX - rect.left - rect.width / 2;
      const offsetY = centerY - rect.top - rect.height / 2;
      const sizeRatio = boundedWidth / state.imageDimensions.width;
      newX = state.position.x - offsetX * (sizeRatio - 1);
      newY = state.position.y - offsetY * (sizeRatio - 1);
    }
    const containerWidth = containerRef.current.offsetWidth;
    const containerHeight = containerRef.current.offsetHeight;
    const isWiderThanContainer = boundedWidth - containerWidth >= 1;
    const isTallerThanContainer = boundedHeight - containerHeight >= 1;
    if (isWiderThanContainer) {
      const maxX = (boundedWidth - containerWidth) / 2;
      const minX = -maxX;
      newX = Math.min(Math.max(newX, minX), maxX);
    } else {
      newX = 0;
    }
    if (isTallerThanContainer) {
      const maxY = (boundedHeight - containerHeight) / 2;
      const minY = -maxY;
      newY = Math.min(Math.max(newY, minY), maxY);
    } else {
      newY = 0;
    }
    state.position = {
      x: newX,
      y: newY
    };
    state.imageDimensions = {
      width: boundedWidth,
      height: boundedHeight
    };
    updateTransform();
    updateContainerCursor();
  };
  const zoomIn = () => {
    const state = stateRef.current;
    if (!state.initialDimensions.width) return;
    const absoluteMaxWidth = state.initialDimensions.width * maxScale;
    if (state.imageDimensions.width >= absoluteMaxWidth) return;
    const relativeZoomStep = state.initialDimensions.width * zoomStep;
    handleZoom(state.imageDimensions.width + relativeZoomStep);
  };
  const zoomOut = () => {
    const state = stateRef.current;
    if (!state.initialDimensions.width) return;
    if (state.imageDimensions.width <= state.initialDimensions.width) return;
    const relativeZoomStep = state.initialDimensions.width * zoomStep;
    handleZoom(state.imageDimensions.width - relativeZoomStep);
  };
  const resetTransform = () => {
    const state = stateRef.current;
    if (!state.initialDimensions.width || !state.initialDimensions.height) return;
    if (state.imageDimensions.width === state.initialDimensions.width && state.position.x === 0 && state.position.y === 0) return;
    state.imageDimensions = {
      ...state.initialDimensions
    };
    state.position = {
      x: 0,
      y: 0
    };
    updateTransform();
    updateContainerCursor();
  };
  const handleWheel = e => {
    e.preventDefault();
    e.stopPropagation();
    const state = stateRef.current;
    if (!state.imageDimensions.width) return;
    if (e.ctrlKey) {
      const zoomFactor = 1 - e.deltaY * 0.01;
      const newWidth = state.imageDimensions.width * zoomFactor;
      handleZoom(newWidth, e.clientX, e.clientY);
      return;
    }
    const delta = -e.deltaY * 0.001;
    const sizeChange = state.imageDimensions.width * delta;
    const newWidth = state.imageDimensions.width + sizeChange;
    handleZoom(newWidth, e.clientX, e.clientY);
  };
  const getTouchDistance = touches => {
    const dx = touches[0].clientX - touches[1].clientX;
    const dy = touches[0].clientY - touches[1].clientY;
    return Math.sqrt(dx * dx + dy * dy);
  };
  const handleTouchStart = e => {
    const state = stateRef.current;
    if (e.touches.length === 2) {
      e.preventDefault();
      state.isZooming = true;
      state.initialDistance = getTouchDistance(e.touches);
    } else if (e.touches.length === 1) {
      if (!containerRef.current || !state.imageDimensions.width || !state.imageDimensions.height) return;
      const containerWidth = containerRef.current.offsetWidth;
      const containerHeight = containerRef.current.offsetHeight;
      const imgWidth = state.imageDimensions.width;
      const imgHeight = state.imageDimensions.height;
      const isOverflowing = imgWidth - containerWidth >= 1 || imgHeight - containerHeight >= 1;
      if (!isOverflowing) return;
      state.isDragging = true;
      state.dragStart = {
        x: e.touches[0].clientX - state.position.x,
        y: e.touches[0].clientY - state.position.y
      };
      updateContainerCursor();
    }
  };
  const handleTouchMove = e => {
    const state = stateRef.current;
    if (e.touches.length === 2 && state.isZooming) {
      e.preventDefault();
      const currentDistance = getTouchDistance(e.touches);
      const sizeDelta = currentDistance / state.initialDistance;
      const newWidth = state.imageDimensions.width * sizeDelta;
      const centerX = (e.touches[0].clientX + e.touches[1].clientX) / 2;
      const centerY = (e.touches[0].clientY + e.touches[1].clientY) / 2;
      handleZoom(newWidth, centerX, centerY);
      state.initialDistance = currentDistance;
    } else if (e.touches.length === 1 && state.isDragging && containerRef.current) {
      let newX = e.touches[0].clientX - state.dragStart.x;
      let newY = e.touches[0].clientY - state.dragStart.y;
      if (state.imageDimensions.width && state.imageDimensions.height) {
        const containerWidth = containerRef.current.offsetWidth;
        const containerHeight = containerRef.current.offsetHeight;
        const imgWidth = state.imageDimensions.width;
        const imgHeight = state.imageDimensions.height;
        const isWider = imgWidth - containerWidth >= 1;
        const isTaller = imgHeight - containerHeight >= 1;
        if (isWider) {
          const halfOverflowX = (imgWidth - containerWidth) / 2;
          newX = Math.min(Math.max(newX, -halfOverflowX), halfOverflowX);
        } else {
          newX = 0;
        }
        if (isTaller) {
          const halfOverflowY = (imgHeight - containerHeight) / 2;
          newY = Math.min(Math.max(newY, -halfOverflowY), halfOverflowY);
        } else {
          newY = 0;
        }
      }
      state.position = {
        x: newX,
        y: newY
      };
      updateTransform();
    }
  };
  const handleTouchEnd = e => {
    const state = stateRef.current;
    if (e.touches.length < 2) {
      state.isZooming = false;
    }
    if (e.touches.length === 0) {
      state.isDragging = false;
      updateContainerCursor();
    }
  };
  React.useEffect(() => {
    const handleEscape = e => {
      if (e.key === 'Escape' && showLightbox) {
        closeLightbox();
      }
    };
    const preventScroll = e => {
      e.preventDefault();
      e.stopPropagation();
      return false;
    };
    if (showLightbox) {
      const originalOverflow = document.body.style.overflow;
      const originalPosition = document.body.style.position;
      const scrollY = window.scrollY;
      document.body.style.overflow = 'hidden';
      document.body.style.position = 'fixed';
      document.body.style.top = `-${scrollY}px`;
      document.body.style.width = '100%';
      window.addEventListener('wheel', preventScroll, {
        passive: false
      });
      window.addEventListener('touchmove', preventScroll, {
        passive: false
      });
      window.addEventListener('scroll', preventScroll, {
        passive: false
      });
      document.addEventListener('keydown', handleEscape);
      document.addEventListener('mousemove', handleMouseMove);
      document.addEventListener('mouseup', handleMouseUp);
      return () => {
        document.body.style.overflow = originalOverflow;
        document.body.style.position = originalPosition;
        document.body.style.top = '';
        document.body.style.width = '';
        window.scrollTo(0, scrollY);
        window.removeEventListener('wheel', preventScroll);
        window.removeEventListener('touchmove', preventScroll);
        window.removeEventListener('scroll', preventScroll);
        document.removeEventListener('keydown', handleEscape);
        document.removeEventListener('mousemove', handleMouseMove);
        document.removeEventListener('mouseup', handleMouseUp);
      };
    }
  }, [showLightbox]);
  React.useEffect(() => {
    if (!showLightbox || !containerRef.current) return;
    const el = containerRef.current;
    const state = stateRef.current;
    const onGestureStart = e => {
      e.preventDefault();
      e.stopPropagation();
      state.isZooming = true;
      state.gestureBaseWidth = state.imageDimensions.width || state.initialDimensions.width || 0;
    };
    const onGestureChange = e => {
      e.preventDefault();
      e.stopPropagation();
      if (!state.gestureBaseWidth) return;
      const newWidth = state.gestureBaseWidth * e.scale;
      const cx = e.clientX ?? el.getBoundingClientRect().left + el.getBoundingClientRect().width / 2;
      const cy = e.clientY ?? el.getBoundingClientRect().top + el.getBoundingClientRect().height / 2;
      handleZoom(newWidth, cx, cy);
    };
    const onGestureEnd = e => {
      e.preventDefault();
      e.stopPropagation();
      state.isZooming = false;
      state.gestureBaseWidth = 0;
    };
    const onWheelCapture = e => {
      if (e.ctrlKey) {
        e.preventDefault();
        e.stopPropagation();
        const zoomFactor = 1 - e.deltaY * 0.01;
        const newWidth = state.imageDimensions.width * zoomFactor;
        handleZoom(newWidth, e.clientX, e.clientY);
      }
    };
    el.addEventListener('gesturestart', onGestureStart, {
      passive: false
    });
    el.addEventListener('gesturechange', onGestureChange, {
      passive: false
    });
    el.addEventListener('gestureend', onGestureEnd, {
      passive: false
    });
    el.addEventListener('wheel', onWheelCapture, {
      passive: false
    });
    return () => {
      el.removeEventListener('gesturestart', onGestureStart);
      el.removeEventListener('gesturechange', onGestureChange);
      el.removeEventListener('gestureend', onGestureEnd);
      el.removeEventListener('wheel', onWheelCapture);
    };
  }, [showLightbox]);
  return <>
            <div className="flex flex-col">
                <div onClick={e => {
    e.preventDefault();
    e.stopPropagation();
  }}>
                    <Frame caption={caption}>
                        <div onClick={handleImageClick} onKeyDown={e => {
    if (e.key === 'Enter' || e.key === ' ') {
      handleImageClick(e);
    }
  }} role="button" tabIndex={0} aria-label={`Enlarge diagram: ${caption}`} className="cursor-zoom-in" style={{
    display: 'block',
    position: 'relative'
  }}>
                            <img ref={imgRef} src={imageURL} alt={caption || imageName} className="w-full h-auto" style={{
    pointerEvents: 'none'
  }} />
                        </div>
                    </Frame>
                </div>
                <div className="flex justify-end my-2">
                    <Tooltip tip="Download image">
                        <button type="button" aria-label={`Download ${caption}`} onClick={handleDownload} className="bg-white text-black text-xs px-3 py-2 rounded-md border border-gray-300 hover:bg-gray-100 transition-all cursor-pointer">
                            <Icon icon="arrow-down-to-line" iconType="regular" color="black" /> 
                        </button>
                    </Tooltip>
                </div>
            </div>

            {showLightbox && <div role="dialog" aria-modal="true" aria-label={caption} style={{
    position: 'fixed',
    top: 0,
    left: 0,
    width: '100%',
    height: '100%',
    backgroundColor: 'rgba(0, 0, 0, 0.95)',
    zIndex: 999999,
    display: 'flex',
    alignItems: 'center',
    cursor: 'default',
    justifyContent: 'center',
    padding: `${Math.round(viewportPadding / 2)}px`,
    touchAction: 'none'
  }} onWheel={e => {
    e.preventDefault();
    e.stopPropagation();
  }}>
                    <div ref={containerRef} style={{
    position: 'relative',
    width: '100%',
    height: '100%',
    minWidth: `calc(100vw - ${viewportPadding}px)`,
    minHeight: `calc(100vh - ${viewportPadding}px)`,
    maxWidth: '100%',
    maxHeight: '100%',
    overflow: 'hidden',
    cursor: 'wait',
    display: 'flex',
    alignItems: 'center',
    justifyContent: 'center',
    borderRadius: '15px',
    border: '2px solid rgba(255, 255, 255, 0.3)',
    touchAction: 'none',
    backgroundColor: containerBackground,
    userSelect: 'none',
    WebkitUserSelect: 'none',
    MozUserSelect: 'none',
    msUserSelect: 'none',
    WebkitTouchCallout: 'none',
    WebkitTapHighlightColor: 'transparent'
  }} onMouseDown={handleMouseDown} onWheel={handleWheel} onTouchStart={handleTouchStart} onTouchMove={handleTouchMove} onTouchEnd={handleTouchEnd}>
                        <div className="caption-container" style={{
    position: 'absolute',
    left: '20px',
    top: '20px',
    zIndex: 1000000,
    backgroundColor: 'rgba(36, 36, 36, 0.76)',
    padding: '10px',
    borderRadius: '15px',
    border: '1px solid rgba(255, 255, 255, 0.3)',
    pointerEvents: 'none'
  }}>
                            <p className="caption-text" style={{
    fontSize: '16px',
    fontWeight: 'bold',
    color: 'white',
    marginLeft: '10px'
  }}>
                                {caption}
                            </p>
                        </div>
                        <button type="button" aria-label="Close enlarged view" onClick={closeLightbox} onMouseDown={e => e.stopPropagation()} style={{
    position: 'absolute',
    top: '20px',
    right: '20px',
    background: 'rgba(36, 36, 36, 0.76)',
    border: '1px solid rgba(255, 255, 255, 0.3)',
    color: 'white',
    fontSize: '24px',
    width: '40px',
    height: '40px',
    borderRadius: '50%',
    cursor: 'pointer',
    display: 'flex',
    alignItems: 'center',
    justifyContent: 'center',
    transition: 'background 0.2s',
    zIndex: 1000000
  }} onMouseEnter={e => {
    e.target.style.background = 'rgba(36, 36, 36, 0.9)';
  }} onMouseLeave={e => {
    e.target.style.background = 'rgba(36, 36, 36, 0.76)';
  }}>
                            ×
                        </button>
                        <div style={{
    position: 'absolute',
    bottom: '20px',
    left: '20px',
    display: 'flex',
    gap: '10px',
    zIndex: 1000000
  }}>
                            <button type="button" aria-label="Zoom in" ref={zoomInBtnRef} onClick={e => {
    e.stopPropagation();
    zoomIn();
  }} onMouseDown={e => e.stopPropagation()} style={{
    background: 'rgba(36, 36, 36, 0.76)',
    border: '1px solid rgba(255, 255, 255, 0.3)',
    color: 'white',
    fontSize: '20px',
    width: '40px',
    height: '40px',
    borderRadius: '8px',
    cursor: 'pointer',
    display: 'flex',
    alignItems: 'center',
    justifyContent: 'center',
    transition: 'background 0.2s',
    opacity: 1
  }} onMouseEnter={e => {
    if (!e.target.disabled) e.target.style.background = 'rgba(36, 36, 36, 0.9)';
  }} onMouseLeave={e => {
    e.target.style.background = 'rgba(36, 36, 36, 0.76)';
  }}>
                                +
                            </button>
                            <button type="button" aria-label="Zoom out" ref={zoomOutBtnRef} onClick={e => {
    e.stopPropagation();
    zoomOut();
  }} onMouseDown={e => e.stopPropagation()} style={{
    background: 'rgba(36, 36, 36, 0.76)',
    border: '1px solid rgba(255, 255, 255, 0.3)',
    color: 'white',
    fontSize: '20px',
    width: '40px',
    height: '40px',
    borderRadius: '8px',
    cursor: 'not-allowed',
    display: 'flex',
    alignItems: 'center',
    justifyContent: 'center',
    transition: 'background 0.2s',
    opacity: 0.5,
    pointerEvents: 'auto'
  }} onMouseEnter={e => {
    if (e.target.style.opacity !== '0.5') {
      e.target.style.background = 'rgba(36, 36, 36, 0.9)';
    }
  }} onMouseLeave={e => {
    e.target.style.background = 'rgba(36, 36, 36, 0.76)';
  }}>
                                −
                            </button>
                            <button type="button" aria-label="Reset zoom" ref={resetBtnRef} onClick={e => {
    e.stopPropagation();
    resetTransform();
  }} onMouseDown={e => e.stopPropagation()} style={{
    background: 'rgba(36, 36, 36, 0.76)',
    border: '1px solid rgba(255, 255, 255, 0.3)',
    color: 'white',
    fontSize: '16px',
    width: '40px',
    height: '40px',
    borderRadius: '8px',
    cursor: 'not-allowed',
    display: 'flex',
    alignItems: 'center',
    justifyContent: 'center',
    transition: 'background 0.2s',
    opacity: 0.5,
    pointerEvents: 'auto'
  }} onMouseEnter={e => {
    if (e.target.style.opacity !== '0.5') {
      e.target.style.background = 'rgba(36, 36, 36, 0.9)';
    }
  }} onMouseLeave={e => {
    e.target.style.background = 'rgba(36, 36, 36, 0.76)';
  }}>
                                ⟲
                            </button>
                            <div ref={scaleDisplayRef} style={{
    background: 'rgba(36, 36, 36, 0.76)',
    border: '1px solid rgba(255, 255, 255, 0.3)',
    color: 'white',
    fontSize: '14px',
    padding: '0 12px',
    height: '40px',
    borderRadius: '8px',
    display: 'flex',
    alignItems: 'center',
    justifyContent: 'center',
    minWidth: '60px',
    pointerEvents: 'none'
  }}>
                                100%
                            </div>
                        </div>
                        <div ref={dragHintRef} className="drag-hint" style={{
    display: 'none',
    position: 'absolute',
    right: '20px',
    bottom: '20px',
    zIndex: 1000000,
    backgroundColor: 'rgba(36, 36, 36, 0.76)',
    padding: '10px 15px',
    borderRadius: '15px',
    border: '1px solid rgba(255, 255, 255, 0.3)',
    pointerEvents: 'none'
  }}>
                            <div style={{
    display: 'flex',
    alignItems: 'center',
    gap: '8px'
  }}>
                                <Icon icon="hand" iconType="regular" color="white" />
                                <p style={{
    fontSize: '12px',
    color: 'white',
    margin: 0
  }}>Drag to move image</p>
                            </div>
                        </div>
                        <div ref={imageTransformRef} style={{
    position: 'relative',
    transform: 'translate(0px, 0px)',
    transition: 'none',
    willChange: 'transform',
    flexShrink: 0,
    minWidth: 0,
    minHeight: 0
  }}>
                            <img src={imageURL} alt={caption || imageName} onLoad={e => {
    const state = stateRef.current;
    const naturalWidth = e.target.naturalWidth;
    const naturalHeight = e.target.naturalHeight;
    if (!containerRef.current) return;
    const containerWidth = containerRef.current.offsetWidth;
    const containerHeight = containerRef.current.offsetHeight;
    const scaleX = containerWidth / naturalWidth;
    const scaleY = containerHeight / naturalHeight;
    const initialScale = Math.min(scaleX, scaleY, 1);
    const initialWidth = naturalWidth * initialScale;
    const initialHeight = naturalHeight * initialScale;
    state.naturalDimensions = {
      width: naturalWidth,
      height: naturalHeight
    };
    state.initialDimensions = {
      width: initialWidth,
      height: initialHeight
    };
    state.imageDimensions = {
      width: initialWidth,
      height: initialHeight
    };
    setTimeout(() => {
      state.imageLoaded = true;
      updateTransform();
      updateContainerCursor();
      updateDragHint();
      if (e.target) {
        e.target.style.opacity = '1';
      }
    }, 100);
  }} style={{
    display: 'block',
    width: 'auto',
    height: 'auto',
    maxWidth: 'none',
    maxHeight: 'none',
    opacity: 0,
    transition: 'opacity 0.2s ease-in-out',
    pointerEvents: 'none',
    userSelect: 'none',
    WebkitUserSelect: 'none',
    MozUserSelect: 'none',
    msUserSelect: 'none',
    WebkitTouchCallout: 'none',
    WebkitTapHighlightColor: 'transparent',
    WebkitDrag: 'none',
    WebkitUserDrag: 'none',
    KhtmlUserSelect: 'none'
  }} draggable="false" />
                        </div>
                    </div>
                </div>}
        </>;
};

## Passkey Creation on Device without Access to existing Passkeys

This flow applies when server state shows existing passkeys, the user completes an approved fallback and available history indicates that those passkeys could not have been created on the current device. They may still be available through a syncing provider, so the offer remains optional.

<ToolingFrame imageURL="/images/authentication-flow/web-app/desktop/append-on-second-device-macos.webp" caption="Representative web passkey creation flow on a second device shown on macOS" imageName="append-on-second-device-macos.webp" />

<Steps>
  <Step title="User enters identifier">
    * User enters their identifier (email/username) on the login page.
    * System now knows which user account is attempting to log in.
  </Step>

  <Step title="System identifies passkeys created elsewhere">
    * The server shows at least one registered passkey, but the existing passkeys were created before the current device was first seen.
    * A synced passkey may still be available through the current credential provider, so this history does not prove that passkey login will fail.
    * If the user cannot sign in with a passkey, the system proceeds to an approved fallback.
  </Step>

  <Step title="Complete approved fallback or recovery">
    * The user completes an approved policy-compliant fallback or recovery for the same account.
  </Step>

  <Step title="Passkey append initiates on current device">
    * After successful fallback, the system optionally offers to create an additional passkey through the current platform or credential provider.
    * The user can decline on a shared or temporary device without losing the authenticated session.
  </Step>

  <Step title="System passkey-creation sheet appears">
    * The user authorizes creation with a platform-supported device-unlock method.
  </Step>

  <Step title="Passkey creation successful">
    * The relying party verifies and adds the new passkey.
    * The credential becomes available according to the selected provider's storage and sync behavior; the application does not guarantee device-only or multi-device storage.
  </Step>

  <Step title="Redirection to app dashboard">
    * User is redirected to the app dashboard.
  </Step>
</Steps>

## Acceptance criteria

| Benchmark as | User reach |
| ------------ | ---------- |
| Conditional  | Mid        |

<Note>
  WebAuthn cannot reliably list which account passkeys are usable in the
  current browser. Creation history can show that the existing passkeys were
  created before this device was first seen, but a synced passkey may still be
  available here. The offer therefore remains optional.
</Note>

| ID            | Level   | Acceptance criterion                                                                                                                                                                                                                                                                                                                                                                                                                            | How to verify                                                                                                                                                                                                                                                                                                 |
| ------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **W2.6-AC01** | Core    | This secondary-device flow is considered only when the account already has at least one registered passkey, the current session completed through an allowed non-passkey fallback, and available history indicates that the existing passkeys could not have been created on this device, for example because they were registered before the device was first seen. An account with no passkeys is routed to first-passkey enrollment instead. | Test an account with no passkeys, one with a passkey created on the current device, and one whose passkeys were registered before the current device was first seen and therefore uses fallback. Confirm the three cases route to first-passkey enrollment, normal signed-in use, and this flow respectively. |
| **W2.6-AC02** | Core    | Duplicate and concurrent registration attempts are harmless. `excludeCredentials` prevents avoidable duplicates, while `InvalidStateError` or other evidence that the provider already has the credential triggers a refresh or no-op instead of a user-facing failure.                                                                                                                                                                         | Run concurrent registrations and a journey where the provider already contains a matching credential. Confirm that there is no duplicate server record, blocking error or repeated prompt.                                                                                                                    |
| **W2.6-AC03** | Core    | The stored credential can complete a later passkey sign-in from the target environment.                                                                                                                                                                                                                                                                                                                                                         | Complete creation and perform a later sign-in from the target environment with the added credential.                                                                                                                                                                                                          |
| **W2.6-AC04** | Core    | Registration targets the current client's platform or passkey provider, using client-device guidance and the compatible platform-authenticator setting where supported.                                                                                                                                                                                                                                                                         | Inspect creation options across supported clients. Confirm the current-client creation route is promoted instead of a QR or security-key route.                                                                                                                                                               |
| **W2.6-AC05** | Quality | The offer identifies this as an additional passkey and explains the future same-environment benefit. It does not describe the operation as moving an existing passkey or guarantee that the credential is stored only on the physical device rather than a syncing provider.                                                                                                                                                                    | Review rendered and localized copy on a personal and one-off device. Test users should distinguish additional coverage from first enrollment and understand that provider syncing may apply.                                                                                                                  |
| **W2.6-AC06** | Quality | Registration options contain every server-known account credential ID in `excludeCredentials`, and the server still rejects a duplicate returned by the client.                                                                                                                                                                                                                                                                                 | Generate options for an account with several known credentials and inspect `excludeCredentials`. Every known ID is present. Then return a duplicate and confirm that the server rejects it.                                                                                                                   |

### References

* **Relevant criteria:** **W2.6-AC01 and W2.6-AC05:** [How to get high passkey adoption in creation flows](https://www.corbado.com/blog/passkey-creation-best-practices) covers secondary-device eligibility and differentiated additional-credential messaging.
* **Relevant criteria:** **W2.6-AC01, W2.6-AC03 and W2.6-AC05:** [Payment provider passkeys: multi-device coverage](https://www.corbado.com/blog/payment-provider-passkeys-third-party-sdk) covers first versus additional credentials, device-healing journeys, and later device coverage.
* **Relevant criteria:** **W2.6-AC02–AC04 and W2.6-AC06:** [Create a passkey for passwordless logins](https://web.dev/articles/passkey-registration) covers duplicate handling, response processing, later use and current-client targeting.
* **Relevant criteria:** **W2.6-AC03–AC05:** [Synced Passkey Deployment: Emerging Practices](https://fidoalliance.org/wp-content/uploads/2024/05/Synced-Passkey-Deployment_-Emerging-Practices-for-Consumer-Use-Cases_2024-May-31.pdf) covers syncing semantics, later authentication, current-platform registration and additional-credential messaging.
* **Relevant criteria:** **W2.6-AC02–AC04 and W2.6-AC06:** [Server-side passkey registration](https://developers.google.com/identity/passkeys/developer-guides/server-registration) covers duplicate prevention, response verification, multiple credentials and future authentication data.
