@greatapps/common 1.1.774 → 1.1.776
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.
- package/dist/components/modals/cards/CannotDeleteCardModal.mjs +2 -1
- package/dist/components/modals/cards/CannotDeleteCardModal.mjs.map +1 -1
- package/dist/components/ui/data-display/PaymentInfoCard.mjs +1 -1
- package/dist/components/ui/data-display/PaymentInfoCard.mjs.map +1 -1
- package/dist/modules/auth/utils/assert-management-permission.mjs +3 -2
- package/dist/modules/auth/utils/assert-management-permission.mjs.map +1 -1
- package/dist/modules/auth/utils/assert-management-subscription.mjs +3 -2
- package/dist/modules/auth/utils/assert-management-subscription.mjs.map +1 -1
- package/dist/modules/cards/hooks/cards.hook.mjs +1 -1
- package/dist/modules/cards/hooks/cards.hook.mjs.map +1 -1
- package/dist/modules/cards/hooks/handle-delete-card.hook.mjs +2 -1
- package/dist/modules/cards/hooks/handle-delete-card.hook.mjs.map +1 -1
- package/package.json +1 -1
- package/src/components/modals/cards/CannotDeleteCardModal.tsx +5 -4
- package/src/components/ui/data-display/PaymentInfoCard.tsx +1 -1
- package/src/modules/auth/utils/assert-management-permission.ts +14 -2
- package/src/modules/auth/utils/assert-management-subscription.ts +13 -2
- package/src/modules/cards/hooks/cards.hook.ts +3 -1
- package/src/modules/cards/hooks/handle-delete-card.hook.ts +3 -1
|
@@ -27,7 +27,8 @@ function CannotDeleteCardModal() {
|
|
|
27
27
|
const handleSelectCard = useCallback(
|
|
28
28
|
async (card) => {
|
|
29
29
|
if (!activeSubscription || !cardToDelete) return;
|
|
30
|
-
const
|
|
30
|
+
const subscriptionCardId = activeSubscription.id_card?.toString() ?? "";
|
|
31
|
+
const migratesVindiToStripe = /^\d+$/.test(subscriptionCardId) && !card.is_vindi;
|
|
31
32
|
try {
|
|
32
33
|
await updatePayment({
|
|
33
34
|
subscriptionId: activeSubscription.id,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/components/modals/cards/CannotDeleteCardModal.tsx"],"sourcesContent":["'use client';\n\nimport { useCallback } from 'react';\nimport { Dialog, DialogContent, DialogTitle } from '../../ui/overlay/Dialog';\nimport { Button } from '../../ui/buttons/Button';\nimport { Toast } from '../../ui/feedback/Toast';\nimport { CardItem } from '../../ui/data-display/CardItem';\nimport { IconAlertTriangle, IconPlus } from '@tabler/icons-react';\nimport { toast } from 'sonner';\nimport { useModalManager } from '../../../store/useModalManager';\nimport { useCards } from '../../../modules/cards/hooks/cards.hook';\nimport { useActiveSubscription } from '../../../modules/subscriptions/hooks/find-active-subscription.hook';\nimport { useUpdateSubscriptionPayment } from '../../../modules/subscriptions/hooks/update-subscription-payment.hook';\nimport type { Card } from '../../../modules/cards/types';\nimport { useTranslations } from 'next-intl';\n\nexport function CannotDeleteCardModal() {\n const translate = useTranslations();\n const { activeModal, modalData, closeModal, openModal } = useModalManager();\n const { data: cardsData } = useCards({ includeVindi: true });\n const { mutateAsync: updatePayment } = useUpdateSubscriptionPayment();\n const { data: subscriptionData } = useActiveSubscription();\n const activeSubscription = subscriptionData?.data?.[0] ?? null;\n\n const isOpen = activeModal === 'cannotDeleteCardModal';\n const cardToDeleteId = modalData.cardId as string | undefined;\n\n const allCards = cardsData?.data ?? [];\n const cardToDelete = allCards.find((c) => c.id.toString() === cardToDeleteId);\n const availableCards = allCards.filter((c) => c.id.toString() !== cardToDeleteId);\n\n const handleSelectCard = useCallback(\n async (card: Card) => {\n if (!activeSubscription || !cardToDelete) return;\n\n /* Assinatura
|
|
1
|
+
{"version":3,"sources":["../../../../src/components/modals/cards/CannotDeleteCardModal.tsx"],"sourcesContent":["'use client';\n\nimport { useCallback } from 'react';\nimport { Dialog, DialogContent, DialogTitle } from '../../ui/overlay/Dialog';\nimport { Button } from '../../ui/buttons/Button';\nimport { Toast } from '../../ui/feedback/Toast';\nimport { CardItem } from '../../ui/data-display/CardItem';\nimport { IconAlertTriangle, IconPlus } from '@tabler/icons-react';\nimport { toast } from 'sonner';\nimport { useModalManager } from '../../../store/useModalManager';\nimport { useCards } from '../../../modules/cards/hooks/cards.hook';\nimport { useActiveSubscription } from '../../../modules/subscriptions/hooks/find-active-subscription.hook';\nimport { useUpdateSubscriptionPayment } from '../../../modules/subscriptions/hooks/update-subscription-payment.hook';\nimport type { Card } from '../../../modules/cards/types';\nimport { useTranslations } from 'next-intl';\n\nexport function CannotDeleteCardModal() {\n const translate = useTranslations();\n const { activeModal, modalData, closeModal, openModal } = useModalManager();\n const { data: cardsData } = useCards({ includeVindi: true });\n const { mutateAsync: updatePayment } = useUpdateSubscriptionPayment();\n const { data: subscriptionData } = useActiveSubscription();\n const activeSubscription = subscriptionData?.data?.[0] ?? null;\n\n const isOpen = activeModal === 'cannotDeleteCardModal';\n const cardToDeleteId = modalData.cardId as string | undefined;\n\n const allCards = cardsData?.data ?? [];\n const cardToDelete = allCards.find((c) => c.id.toString() === cardToDeleteId);\n const availableCards = allCards.filter((c) => c.id.toString() !== cardToDeleteId);\n\n const handleSelectCard = useCallback(\n async (card: Card) => {\n if (!activeSubscription || !cardToDelete) return;\n\n /* Assinatura no trilho Vindi (id_card numérico) trocando pra um cartão Stripe:\n * enviar payment_method_id (pm_) dispara a migração Vindi→Stripe no backend —\n * id_card com pm_ cairia no fluxo Vindi, que só aceita id numérico.\n * Nos demais casos (Vindi→Vindi, Stripe→Stripe) o id_card atual já resolve. */\n const subscriptionCardId = activeSubscription.id_card?.toString() ?? '';\n const migratesVindiToStripe = /^\\d+$/.test(subscriptionCardId) && !card.is_vindi;\n\n try {\n await updatePayment({\n subscriptionId: activeSubscription.id,\n data: migratesVindiToStripe\n ? { payment_method_id: card.gateway_card, payment_method: 1 }\n : { id_card: card.id.toString(), payment_method: 1 },\n });\n\n closeModal();\n openModal('deleteCardModal', {\n cardId: cardToDelete.id.toString(),\n });\n } catch (error) {\n toast.custom(\n (t) => (\n <Toast\n variant=\"error\"\n message={\n error instanceof Error\n ? error.message\n : translate('common.cards.cannotDelete.errorUpdateCard')\n }\n toastId={t}\n />\n ),\n { duration: 5000 },\n );\n }\n },\n [updatePayment, activeSubscription, closeModal, openModal, cardToDelete],\n );\n\n const handleAddCard = useCallback(() => {\n closeModal();\n openModal('addCardModal', { showDefaultToggle: true });\n }, [openModal, closeModal]);\n\n return (\n <Dialog open={isOpen} onOpenChange={(open) => !open && closeModal()}>\n <DialogTitle className=\"sr-only\">\n {translate('common.cards.cannotDelete.title')}\n </DialogTitle>\n <DialogContent className=\"flex flex-col p-0 max-w-full sm:max-w-full border-0 rounded-t-2xl rounded-b-none h-dvh top-0 bottom-0 left-0 right-0 translate-x-0 translate-y-0 lg:max-w-[436px] lg:h-auto lg:max-h-[90vh] lg:rounded-lg lg:border lg:top-[50%] lg:left-[50%] lg:right-auto lg:bottom-auto lg:translate-x-[-50%] lg:translate-y-[-50%] overflow-hidden\">\n <div className=\"flex flex-col gap-5 p-4 lg:p-5\">\n <div className=\"flex items-center justify-center size-10 bg-red-50 rounded-lg\">\n <IconAlertTriangle className=\"size-6 text-red-500\" />\n </div>\n\n <div className=\"flex flex-col gap-2\">\n <h2 className=\"paragraph-medium-semibold text-zinc-950\">\n {translate('common.cards.cannotDelete.title')}\n </h2>\n <p className=\"paragraph-small-regular text-zinc-600\">\n {translate('common.cards.cannotDelete.description')}\n </p>\n </div>\n </div>\n\n {availableCards.length > 0 && (\n <div className=\"flex flex-col gap-3 px-4 lg:px-5 pb-4 lg:pb-5 overflow-y-auto max-h-[200px] flex-1 lg:flex-none\">\n <span className=\"paragraph-xsmall-semibold text-zinc-600\">\n {translate('common.cards.cannotDelete.selectDefault')}\n </span>\n {availableCards.map((card) => (\n <CardItem\n key={card.id}\n card={card}\n onSelect={() => handleSelectCard(card)}\n showMenu={false}\n />\n ))}\n </div>\n )}\n\n <div className=\"flex flex-row gap-2 p-4 lg:p-5 border-t border-zinc-200 mt-auto lg:mt-0\">\n <Button onClick={handleAddCard} className=\"h-10! flex-1\">\n <IconPlus size={20} />\n {translate('common.cards.cannotDelete.addNewCard')}\n </Button>\n <Button\n variant=\"secondary\"\n onClick={() => closeModal()}\n className=\"h-10! flex-1\"\n >\n {translate('common.actions.cancel')}\n </Button>\n </div>\n </DialogContent>\n </Dialog>\n );\n}\n"],"mappings":";AAyDY,cAiCF,YAjCE;AAvDZ,SAAS,mBAAmB;AAC5B,SAAS,QAAQ,eAAe,mBAAmB;AACnD,SAAS,cAAc;AACvB,SAAS,aAAa;AACtB,SAAS,gBAAgB;AACzB,SAAS,mBAAmB,gBAAgB;AAC5C,SAAS,aAAa;AACtB,SAAS,uBAAuB;AAChC,SAAS,gBAAgB;AACzB,SAAS,6BAA6B;AACtC,SAAS,oCAAoC;AAE7C,SAAS,uBAAuB;AAEzB,SAAS,wBAAwB;AACtC,QAAM,YAAY,gBAAgB;AAClC,QAAM,EAAE,aAAa,WAAW,YAAY,UAAU,IAAI,gBAAgB;AAC1E,QAAM,EAAE,MAAM,UAAU,IAAI,SAAS,EAAE,cAAc,KAAK,CAAC;AAC3D,QAAM,EAAE,aAAa,cAAc,IAAI,6BAA6B;AACpE,QAAM,EAAE,MAAM,iBAAiB,IAAI,sBAAsB;AACzD,QAAM,qBAAqB,kBAAkB,OAAO,CAAC,KAAK;AAE1D,QAAM,SAAS,gBAAgB;AAC/B,QAAM,iBAAiB,UAAU;AAEjC,QAAM,WAAW,WAAW,QAAQ,CAAC;AACrC,QAAM,eAAe,SAAS,KAAK,CAAC,MAAM,EAAE,GAAG,SAAS,MAAM,cAAc;AAC5E,QAAM,iBAAiB,SAAS,OAAO,CAAC,MAAM,EAAE,GAAG,SAAS,MAAM,cAAc;AAEhF,QAAM,mBAAmB;AAAA,IACvB,OAAO,SAAe;AACpB,UAAI,CAAC,sBAAsB,CAAC,aAAc;AAM1C,YAAM,qBAAqB,mBAAmB,SAAS,SAAS,KAAK;AACrE,YAAM,wBAAwB,QAAQ,KAAK,kBAAkB,KAAK,CAAC,KAAK;AAExE,UAAI;AACF,cAAM,cAAc;AAAA,UAClB,gBAAgB,mBAAmB;AAAA,UACnC,MAAM,wBACF,EAAE,mBAAmB,KAAK,cAAc,gBAAgB,EAAE,IAC1D,EAAE,SAAS,KAAK,GAAG,SAAS,GAAG,gBAAgB,EAAE;AAAA,QACvD,CAAC;AAED,mBAAW;AACX,kBAAU,mBAAmB;AAAA,UAC3B,QAAQ,aAAa,GAAG,SAAS;AAAA,QACnC,CAAC;AAAA,MACH,SAAS,OAAO;AACd,cAAM;AAAA,UACJ,CAAC,MACC;AAAA,YAAC;AAAA;AAAA,cACC,SAAQ;AAAA,cACR,SACE,iBAAiB,QACb,MAAM,UACN,UAAU,2CAA2C;AAAA,cAE3D,SAAS;AAAA;AAAA,UACX;AAAA,UAEF,EAAE,UAAU,IAAK;AAAA,QACnB;AAAA,MACF;AAAA,IACF;AAAA,IACA,CAAC,eAAe,oBAAoB,YAAY,WAAW,YAAY;AAAA,EACzE;AAEA,QAAM,gBAAgB,YAAY,MAAM;AACtC,eAAW;AACX,cAAU,gBAAgB,EAAE,mBAAmB,KAAK,CAAC;AAAA,EACvD,GAAG,CAAC,WAAW,UAAU,CAAC;AAE1B,SACE,qBAAC,UAAO,MAAM,QAAQ,cAAc,CAAC,SAAS,CAAC,QAAQ,WAAW,GAChE;AAAA,wBAAC,eAAY,WAAU,WACpB,oBAAU,iCAAiC,GAC9C;AAAA,IACA,qBAAC,iBAAc,WAAU,2UACvB;AAAA,2BAAC,SAAI,WAAU,kCACb;AAAA,4BAAC,SAAI,WAAU,iEACb,8BAAC,qBAAkB,WAAU,uBAAsB,GACrD;AAAA,QAEA,qBAAC,SAAI,WAAU,uBACb;AAAA,8BAAC,QAAG,WAAU,2CACX,oBAAU,iCAAiC,GAC9C;AAAA,UACA,oBAAC,OAAE,WAAU,yCACV,oBAAU,uCAAuC,GACpD;AAAA,WACF;AAAA,SACF;AAAA,MAEC,eAAe,SAAS,KACvB,qBAAC,SAAI,WAAU,mGACb;AAAA,4BAAC,UAAK,WAAU,2CACb,oBAAU,yCAAyC,GACtD;AAAA,QACC,eAAe,IAAI,CAAC,SACnB;AAAA,UAAC;AAAA;AAAA,YAEC;AAAA,YACA,UAAU,MAAM,iBAAiB,IAAI;AAAA,YACrC,UAAU;AAAA;AAAA,UAHL,KAAK;AAAA,QAIZ,CACD;AAAA,SACH;AAAA,MAGF,qBAAC,SAAI,WAAU,2EACb;AAAA,6BAAC,UAAO,SAAS,eAAe,WAAU,gBACxC;AAAA,8BAAC,YAAS,MAAM,IAAI;AAAA,UACnB,UAAU,sCAAsC;AAAA,WACnD;AAAA,QACA;AAAA,UAAC;AAAA;AAAA,YACC,SAAQ;AAAA,YACR,SAAS,MAAM,WAAW;AAAA,YAC1B,WAAU;AAAA,YAET,oBAAU,uBAAuB;AAAA;AAAA,QACpC;AAAA,SACF;AAAA,OACF;AAAA,KACF;AAEJ;","names":[]}
|
|
@@ -30,7 +30,7 @@ function PaymentInfoCard({
|
|
|
30
30
|
);
|
|
31
31
|
const hasCards = cards.length > 0;
|
|
32
32
|
const hasMoreThan3Cards = cards.length > 3;
|
|
33
|
-
const shouldShowManageButton = isMobile && hasMoreThan3Cards;
|
|
33
|
+
const shouldShowManageButton = isMobile && hasMoreThan3Cards && !!onManageCards;
|
|
34
34
|
const displayedCards = shouldShowManageButton ? cards.slice(0, 3) : cards;
|
|
35
35
|
const handleOpenAddCardModal = useCallback(() => {
|
|
36
36
|
if (onOpenAddCard) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/components/ui/data-display/PaymentInfoCard.tsx"],"sourcesContent":["'use client';\n\nimport { useCallback, useState } from 'react';\nimport { IconChevronDown, IconPlus } from '@tabler/icons-react';\nimport { CardBrandIcon } from './CardBrandIcons';\nimport { CardItem } from './CardItem';\nimport useIsMobile from '../../../hooks/useIsMobile';\nimport { useCards } from '../../../modules/cards/hooks/cards.hook';\nimport { useModalManager } from '../../../store/useModalManager';\nimport { useIsDefaultWhitelabel } from '../../../providers/whitelabel.provider';\nimport type { Card } from '../../../modules/cards/types';\nimport { useTranslations } from 'next-intl';\n\ntype PaymentInfoCardProps = {\n email?: string;\n selectedCardId: string;\n onCardSelect: (cardId: string) => void;\n onManageCards?: () => void;\n onOpenAddCard?: () => void;\n hideDelete?: boolean;\n hideSetDefault?: boolean;\n};\n\nexport function PaymentInfoCard({\n email,\n selectedCardId,\n onCardSelect,\n onManageCards,\n onOpenAddCard,\n hideDelete,\n hideSetDefault,\n}: PaymentInfoCardProps) {\n const translate = useTranslations();\n const isMobile = useIsMobile();\n const { openModal } = useModalManager();\n const isDefaultWl = useIsDefaultWhitelabel();\n\n const [isExpanded, setIsExpanded] = useState(false);\n\n const { data: cardsData } = useCards();\n const cards = cardsData?.data ?? [];\n\n const selectedCard = cards.find(\n (card) => card.id.toString() === selectedCardId.toString(),\n );\n\n const hasCards = cards.length > 0;\n\n const hasMoreThan3Cards = cards.length > 3;\n const shouldShowManageButton = isMobile && hasMoreThan3Cards;\n const displayedCards = shouldShowManageButton ? cards.slice(0, 3) : cards;\n\n const handleOpenAddCardModal = useCallback(() => {\n if (onOpenAddCard) {\n onOpenAddCard();\n return;\n }\n openModal('addCardModal', {\n onCardAdded: (cardId: string) => onCardSelect(cardId),\n });\n }, [openModal, onOpenAddCard, onCardSelect]);\n\n const handleSelectCard = useCallback(\n (cardId: string) => {\n onCardSelect(cardId);\n setIsExpanded(false);\n },\n [onCardSelect],\n );\n\n const handleAfterDelete = useCallback(\n (card: Card) => {\n if (card.id.toString() === selectedCardId.toString()) {\n const remaining = cards.filter((c) => c.id !== card.id);\n if (remaining.length > 0) {\n onCardSelect(remaining[0].id.toString());\n }\n }\n },\n [cards, selectedCardId, onCardSelect],\n );\n\n if (!hasCards) {\n return (\n <div className=\"border border-zinc-200 rounded-lg overflow-hidden\">\n {email && (\n <div className=\"flex items-center h-[60px] px-4 border-b border-zinc-200 gap-3\">\n <span className=\"paragraph-small-medium text-zinc-500 w-[100px]\">{translate('common.cards.paymentInfo.email')}</span>\n <span className=\"paragraph-small-medium text-zinc-950 flex-1\">{email}</span>\n </div>\n )}\n <div className=\"flex items-center h-[60px] px-4 gap-3\">\n <span className=\"paragraph-small-medium text-zinc-500 w-[100px]\">{translate('common.cards.paymentInfo.payment')}</span>\n <button\n type=\"button\"\n onClick={handleOpenAddCardModal}\n className=\"flex items-center h-8 px-2 paragraph-small-semibold text-zinc-950 border border-zinc-200 rounded-md hover:border-zinc-400 transition-colors cursor-pointer w-fit\"\n >\n <IconPlus className=\"size-[18px]\" />\n <span className=\"px-1.5\">{translate('common.cards.paymentInfo.addCard')}</span>\n </button>\n </div>\n </div>\n );\n }\n\n return (\n <div className=\"flex flex-col gap-3\">\n <div className=\"border border-zinc-200 rounded-lg overflow-hidden\">\n {email && (\n <div className=\"flex items-center h-[60px] px-4 border-b border-zinc-200 gap-3\">\n <span className=\"paragraph-small-medium text-zinc-500 w-[100px]\">{translate('common.cards.paymentInfo.email')}</span>\n <span className=\"paragraph-small-medium text-zinc-950 flex-1\">{email}</span>\n </div>\n )}\n\n <div\n className=\"flex items-center px-4 py-4 cursor-pointer gap-3\"\n onClick={() => setIsExpanded((prev) => !prev)}\n >\n {!isExpanded ? (\n <>\n <span className=\"paragraph-small-medium text-zinc-500 w-[100px]\">{translate('common.cards.paymentInfo.payment')}</span>\n <div className=\"flex flex-col gap-0.5 flex-1 min-w-0\">\n <span className=\"paragraph-small-semibold text-zinc-950 truncate\">\n {selectedCard?.name}\n </span>\n <span className=\"paragraph-small-medium text-zinc-950\">\n •••• {selectedCard?.number?.replaceAll(/\\D/g, '')?.slice(-4)}\n </span>\n </div>\n {selectedCard && <CardBrandIcon brand={selectedCard.brand} />}\n <div className=\"size-8 flex items-center justify-center rounded-lg\">\n <IconChevronDown className=\"size-[18px] text-zinc-500\" />\n </div>\n </>\n ) : (\n <>\n <span className=\"paragraph-small-semibold text-zinc-950 flex-1\">\n {translate('common.cards.paymentInfo.selectPaymentMethod')}\n </span>\n <div className=\"size-8 flex items-center justify-center rounded-lg\">\n <IconChevronDown className=\"size-[18px] text-zinc-500 rotate-180\" />\n </div>\n </>\n )}\n </div>\n\n <div\n className={`flex flex-col gap-2 px-4 transition-all duration-300 ease-in-out ${\n isExpanded ? 'max-h-[167px] overflow-y-auto pb-4 opacity-100' : 'max-h-0 overflow-hidden pb-0 opacity-0'\n }`}\n >\n {displayedCards.map((card) => (\n <CardItem\n key={card.id}\n card={card}\n isSelected={card.id.toString() === selectedCardId.toString()}\n onSelect={() => handleSelectCard(card.id.toString())}\n hideDelete={hideDelete}\n hideSetDefault={hideSetDefault}\n onAfterDelete={handleAfterDelete}\n />\n ))}\n {shouldShowManageButton && onManageCards && (\n <button\n type=\"button\"\n onClick={onManageCards}\n className={`flex items-center justify-center h-10 px-4 paragraph-small-semibold rounded-lg transition-colors cursor-pointer mt-2 ${\n isDefaultWl\n ? 'text-cyan-600 border border-cyan-200 hover:bg-cyan-50'\n : 'text-zinc-600 border border-zinc-200 hover:bg-zinc-50'\n }`}\n >\n {translate('common.cards.paymentInfo.manageCards')}\n </button>\n )}\n </div>\n </div>\n\n <button\n type=\"button\"\n onClick={handleOpenAddCardModal}\n className=\"flex items-center h-8 px-2 paragraph-small-semibold text-zinc-950 border border-zinc-200 rounded-md hover:border-zinc-400 transition-colors cursor-pointer w-fit\"\n >\n <IconPlus className=\"size-[18px]\" />\n <span className=\"px-1.5\">\n {isExpanded ? translate('common.cards.paymentInfo.addCard') : translate('common.cards.paymentInfo.newPaymentMethod')}\n </span>\n </button>\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../../src/components/ui/data-display/PaymentInfoCard.tsx"],"sourcesContent":["'use client';\n\nimport { useCallback, useState } from 'react';\nimport { IconChevronDown, IconPlus } from '@tabler/icons-react';\nimport { CardBrandIcon } from './CardBrandIcons';\nimport { CardItem } from './CardItem';\nimport useIsMobile from '../../../hooks/useIsMobile';\nimport { useCards } from '../../../modules/cards/hooks/cards.hook';\nimport { useModalManager } from '../../../store/useModalManager';\nimport { useIsDefaultWhitelabel } from '../../../providers/whitelabel.provider';\nimport type { Card } from '../../../modules/cards/types';\nimport { useTranslations } from 'next-intl';\n\ntype PaymentInfoCardProps = {\n email?: string;\n selectedCardId: string;\n onCardSelect: (cardId: string) => void;\n onManageCards?: () => void;\n onOpenAddCard?: () => void;\n hideDelete?: boolean;\n hideSetDefault?: boolean;\n};\n\nexport function PaymentInfoCard({\n email,\n selectedCardId,\n onCardSelect,\n onManageCards,\n onOpenAddCard,\n hideDelete,\n hideSetDefault,\n}: PaymentInfoCardProps) {\n const translate = useTranslations();\n const isMobile = useIsMobile();\n const { openModal } = useModalManager();\n const isDefaultWl = useIsDefaultWhitelabel();\n\n const [isExpanded, setIsExpanded] = useState(false);\n\n const { data: cardsData } = useCards();\n const cards = cardsData?.data ?? [];\n\n const selectedCard = cards.find(\n (card) => card.id.toString() === selectedCardId.toString(),\n );\n\n const hasCards = cards.length > 0;\n\n const hasMoreThan3Cards = cards.length > 3;\n const shouldShowManageButton = isMobile && hasMoreThan3Cards && !!onManageCards;\n const displayedCards = shouldShowManageButton ? cards.slice(0, 3) : cards;\n\n const handleOpenAddCardModal = useCallback(() => {\n if (onOpenAddCard) {\n onOpenAddCard();\n return;\n }\n openModal('addCardModal', {\n onCardAdded: (cardId: string) => onCardSelect(cardId),\n });\n }, [openModal, onOpenAddCard, onCardSelect]);\n\n const handleSelectCard = useCallback(\n (cardId: string) => {\n onCardSelect(cardId);\n setIsExpanded(false);\n },\n [onCardSelect],\n );\n\n const handleAfterDelete = useCallback(\n (card: Card) => {\n if (card.id.toString() === selectedCardId.toString()) {\n const remaining = cards.filter((c) => c.id !== card.id);\n if (remaining.length > 0) {\n onCardSelect(remaining[0].id.toString());\n }\n }\n },\n [cards, selectedCardId, onCardSelect],\n );\n\n if (!hasCards) {\n return (\n <div className=\"border border-zinc-200 rounded-lg overflow-hidden\">\n {email && (\n <div className=\"flex items-center h-[60px] px-4 border-b border-zinc-200 gap-3\">\n <span className=\"paragraph-small-medium text-zinc-500 w-[100px]\">{translate('common.cards.paymentInfo.email')}</span>\n <span className=\"paragraph-small-medium text-zinc-950 flex-1\">{email}</span>\n </div>\n )}\n <div className=\"flex items-center h-[60px] px-4 gap-3\">\n <span className=\"paragraph-small-medium text-zinc-500 w-[100px]\">{translate('common.cards.paymentInfo.payment')}</span>\n <button\n type=\"button\"\n onClick={handleOpenAddCardModal}\n className=\"flex items-center h-8 px-2 paragraph-small-semibold text-zinc-950 border border-zinc-200 rounded-md hover:border-zinc-400 transition-colors cursor-pointer w-fit\"\n >\n <IconPlus className=\"size-[18px]\" />\n <span className=\"px-1.5\">{translate('common.cards.paymentInfo.addCard')}</span>\n </button>\n </div>\n </div>\n );\n }\n\n return (\n <div className=\"flex flex-col gap-3\">\n <div className=\"border border-zinc-200 rounded-lg overflow-hidden\">\n {email && (\n <div className=\"flex items-center h-[60px] px-4 border-b border-zinc-200 gap-3\">\n <span className=\"paragraph-small-medium text-zinc-500 w-[100px]\">{translate('common.cards.paymentInfo.email')}</span>\n <span className=\"paragraph-small-medium text-zinc-950 flex-1\">{email}</span>\n </div>\n )}\n\n <div\n className=\"flex items-center px-4 py-4 cursor-pointer gap-3\"\n onClick={() => setIsExpanded((prev) => !prev)}\n >\n {!isExpanded ? (\n <>\n <span className=\"paragraph-small-medium text-zinc-500 w-[100px]\">{translate('common.cards.paymentInfo.payment')}</span>\n <div className=\"flex flex-col gap-0.5 flex-1 min-w-0\">\n <span className=\"paragraph-small-semibold text-zinc-950 truncate\">\n {selectedCard?.name}\n </span>\n <span className=\"paragraph-small-medium text-zinc-950\">\n •••• {selectedCard?.number?.replaceAll(/\\D/g, '')?.slice(-4)}\n </span>\n </div>\n {selectedCard && <CardBrandIcon brand={selectedCard.brand} />}\n <div className=\"size-8 flex items-center justify-center rounded-lg\">\n <IconChevronDown className=\"size-[18px] text-zinc-500\" />\n </div>\n </>\n ) : (\n <>\n <span className=\"paragraph-small-semibold text-zinc-950 flex-1\">\n {translate('common.cards.paymentInfo.selectPaymentMethod')}\n </span>\n <div className=\"size-8 flex items-center justify-center rounded-lg\">\n <IconChevronDown className=\"size-[18px] text-zinc-500 rotate-180\" />\n </div>\n </>\n )}\n </div>\n\n <div\n className={`flex flex-col gap-2 px-4 transition-all duration-300 ease-in-out ${\n isExpanded ? 'max-h-[167px] overflow-y-auto pb-4 opacity-100' : 'max-h-0 overflow-hidden pb-0 opacity-0'\n }`}\n >\n {displayedCards.map((card) => (\n <CardItem\n key={card.id}\n card={card}\n isSelected={card.id.toString() === selectedCardId.toString()}\n onSelect={() => handleSelectCard(card.id.toString())}\n hideDelete={hideDelete}\n hideSetDefault={hideSetDefault}\n onAfterDelete={handleAfterDelete}\n />\n ))}\n {shouldShowManageButton && onManageCards && (\n <button\n type=\"button\"\n onClick={onManageCards}\n className={`flex items-center justify-center h-10 px-4 paragraph-small-semibold rounded-lg transition-colors cursor-pointer mt-2 ${\n isDefaultWl\n ? 'text-cyan-600 border border-cyan-200 hover:bg-cyan-50'\n : 'text-zinc-600 border border-zinc-200 hover:bg-zinc-50'\n }`}\n >\n {translate('common.cards.paymentInfo.manageCards')}\n </button>\n )}\n </div>\n </div>\n\n <button\n type=\"button\"\n onClick={handleOpenAddCardModal}\n className=\"flex items-center h-8 px-2 paragraph-small-semibold text-zinc-950 border border-zinc-200 rounded-md hover:border-zinc-400 transition-colors cursor-pointer w-fit\"\n >\n <IconPlus className=\"size-[18px]\" />\n <span className=\"px-1.5\">\n {isExpanded ? translate('common.cards.paymentInfo.addCard') : translate('common.cards.paymentInfo.newPaymentMethod')}\n </span>\n </button>\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import "server-only";
|
|
2
|
+
import { cache } from "react";
|
|
2
3
|
import { UserProfile } from "../../users/schema";
|
|
3
4
|
import { findUserById } from "../../users/action/find-user-by-id.action";
|
|
4
5
|
import { ApiError } from "../../../infra/api/types";
|
|
5
6
|
import { assertManagementSubscription } from "./assert-management-subscription";
|
|
6
|
-
|
|
7
|
+
const getCurrentUserProfile = cache(async () => {
|
|
7
8
|
const result = await findUserById();
|
|
8
9
|
if (!result.success || !result.data) {
|
|
9
10
|
throw new ApiError(
|
|
@@ -13,7 +14,7 @@ async function getCurrentUserProfile() {
|
|
|
13
14
|
);
|
|
14
15
|
}
|
|
15
16
|
return result.data.profile;
|
|
16
|
-
}
|
|
17
|
+
});
|
|
17
18
|
async function assertManagementPermission(permission) {
|
|
18
19
|
const profile = await getCurrentUserProfile();
|
|
19
20
|
switch (permission) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/modules/auth/utils/assert-management-permission.ts"],"sourcesContent":["import 'server-only';\n\nimport { UserProfile } from '../../users/schema';\nimport { findUserById } from '../../users/action/find-user-by-id.action';\nimport { ApiError } from '../../../infra/api/types';\nimport { assertManagementSubscription } from './assert-management-subscription';\nimport type { ManagementPermission } from '../types/management-permission.type';\n\
|
|
1
|
+
{"version":3,"sources":["../../../../src/modules/auth/utils/assert-management-permission.ts"],"sourcesContent":["import 'server-only';\n\nimport { cache } from 'react';\n\nimport { UserProfile } from '../../users/schema';\nimport { findUserById } from '../../users/action/find-user-by-id.action';\nimport { ApiError } from '../../../infra/api/types';\nimport { assertManagementSubscription } from './assert-management-subscription';\nimport type { ManagementPermission } from '../types/management-permission.type';\n\n/**\n * Perfil do usuário autenticado, deduplicado por request via React.cache — mesma razão\n * do `requireValidSession`.\n *\n * Este gate abre TODA leitura e escrita gateada, e o perfil não depende da permissão que\n * está sendo checada: uma request que toca N services pagava N round-trips ao backend para\n * reler a mesma linha. Medido em produção (26/08, carregamento da aba de assinatura de uma\n * conta): ~30 GETs de `/accounts/{id}/users/{id}` em 20 s, 200–880 ms cada, competindo pelo\n * mesmo worker de banco que as chamadas de verdade.\n */\nconst getCurrentUserProfile = cache(async () => {\n const result = await findUserById();\n\n if (!result.success || !result.data) {\n throw new ApiError(\n ('error' in result && result.error) || 'Não foi possível identificar o usuário autenticado',\n 'USER_CONTEXT_FAILED',\n 403\n );\n }\n\n return result.data.profile;\n});\n\nexport async function assertManagementPermission(permission: ManagementPermission) {\n const profile = await getCurrentUserProfile();\n\n switch (permission) {\n case 'manage_pages':\n case 'manage_redirects':\n case 'manage_domains':\n case 'manage_ab_tests':\n case 'restore_pages':\n case 'create_pages':\n case 'update_pages':\n case 'delete_pages':\n if (profile === UserProfile.viewer) {\n throw new ApiError('Você não tem permissão para executar esta ação', 'FORBIDDEN', 403);\n }\n break;\n case 'manage_projects':\n case 'delete_leads':\n case 'view_cards':\n case 'manage_cards':\n case 'manage_subscriptions':\n if (profile !== UserProfile.owner && profile !== UserProfile.admin) {\n throw new ApiError('Você não tem permissão para executar esta ação', 'FORBIDDEN', 403);\n }\n break;\n default:\n throw new ApiError('Permissão inválida', 'INVALID_PERMISSION', 500);\n }\n\n const skipSubscriptionCheck: ManagementPermission[] = ['manage_subscriptions', 'manage_cards', 'view_cards'];\n if (!skipSubscriptionCheck.includes(permission)) {\n await assertManagementSubscription();\n }\n}\n"],"mappings":"AAAA,OAAO;AAEP,SAAS,aAAa;AAEtB,SAAS,mBAAmB;AAC5B,SAAS,oBAAoB;AAC7B,SAAS,gBAAgB;AACzB,SAAS,oCAAoC;AAa7C,MAAM,wBAAwB,MAAM,YAAY;AAC9C,QAAM,SAAS,MAAM,aAAa;AAElC,MAAI,CAAC,OAAO,WAAW,CAAC,OAAO,MAAM;AACnC,UAAM,IAAI;AAAA,MACP,WAAW,UAAU,OAAO,SAAU;AAAA,MACvC;AAAA,MACA;AAAA,IACF;AAAA,EACF;AAEA,SAAO,OAAO,KAAK;AACrB,CAAC;AAED,eAAsB,2BAA2B,YAAkC;AACjF,QAAM,UAAU,MAAM,sBAAsB;AAE5C,UAAQ,YAAY;AAAA,IAClB,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AACH,UAAI,YAAY,YAAY,QAAQ;AAClC,cAAM,IAAI,SAAS,iEAAkD,aAAa,GAAG;AAAA,MACvF;AACA;AAAA,IACF,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AACH,UAAI,YAAY,YAAY,SAAS,YAAY,YAAY,OAAO;AAClE,cAAM,IAAI,SAAS,iEAAkD,aAAa,GAAG;AAAA,MACvF;AACA;AAAA,IACF;AACE,YAAM,IAAI,SAAS,4BAAsB,sBAAsB,GAAG;AAAA,EACtE;AAEA,QAAM,wBAAgD,CAAC,wBAAwB,gBAAgB,YAAY;AAC3G,MAAI,CAAC,sBAAsB,SAAS,UAAU,GAAG;AAC/C,UAAM,6BAA6B;AAAA,EACrC;AACF;","names":[]}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import "server-only";
|
|
2
|
+
import { cache } from "react";
|
|
2
3
|
import { ApiError } from "../../../infra/api/types";
|
|
3
4
|
import { listSubscriptionsAction } from "../../subscriptions/actions/list-subscriptions.action";
|
|
4
5
|
import { getSubscriptionExpiryDate } from "../../subscriptions/utils/get-subscription-expiry-date";
|
|
5
|
-
|
|
6
|
+
const assertManagementSubscription = cache(async () => {
|
|
6
7
|
const result = await listSubscriptionsAction({ active: true });
|
|
7
8
|
if (!result.success) {
|
|
8
9
|
throw new ApiError(
|
|
@@ -28,7 +29,7 @@ async function assertManagementSubscription() {
|
|
|
28
29
|
403
|
|
29
30
|
);
|
|
30
31
|
}
|
|
31
|
-
}
|
|
32
|
+
});
|
|
32
33
|
export {
|
|
33
34
|
assertManagementSubscription
|
|
34
35
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/modules/auth/utils/assert-management-subscription.ts"],"sourcesContent":["import \"server-only\";\n\nimport { ApiError } from \"../../../infra/api/types\";\nimport { listSubscriptionsAction } from \"../../subscriptions/actions/list-subscriptions.action\";\nimport { getSubscriptionExpiryDate } from \"../../subscriptions/utils/get-subscription-expiry-date\";\n\nexport
|
|
1
|
+
{"version":3,"sources":["../../../../src/modules/auth/utils/assert-management-subscription.ts"],"sourcesContent":["import \"server-only\";\n\nimport { cache } from \"react\";\n\nimport { ApiError } from \"../../../infra/api/types\";\nimport { listSubscriptionsAction } from \"../../subscriptions/actions/list-subscriptions.action\";\nimport { getSubscriptionExpiryDate } from \"../../subscriptions/utils/get-subscription-expiry-date\";\n\n/**\n * Deduplicado por request via React.cache, como `getCurrentUserProfile` e `requireValidSession`.\n *\n * A leitura por trás é `/accounts/{id}/subscriptions?limit=1`, e ela é MAIS cara que o gate de\n * perfil: para conta Stripe a listagem consulta a API do gateway ao vivo. Em produção (26/08) o\n * mesmo carregamento disparou quatro dessas em paralelo — 727 ms, 4,2 s, 5,0 s e 7,0 s, contra um\n * p50 de 528 ms para o endpoint. Elas se atrasam entre si: são cópias da mesma pergunta disputando\n * a mesma fila.\n */\nexport const assertManagementSubscription = cache(async () => {\n const result = await listSubscriptionsAction({ active: true });\n\n if (!result.success) {\n throw new ApiError(\n result.error || \"Não foi possível validar a assinatura atual\",\n \"SUBSCRIPTION_CONTEXT_FAILED\",\n 403,\n );\n }\n\n const subscription = result.data?.data?.[0];\n\n if (!subscription) {\n throw new ApiError(\n \"Nenhuma assinatura ativa encontrada. Assine um plano para continuar.\",\n \"SUBSCRIPTION_NOT_FOUND\",\n 403,\n );\n }\n\n const expiryDate = getSubscriptionExpiryDate(subscription);\n const isExpired = !!expiryDate && Date.now() >= expiryDate.getTime();\n\n if (isExpired) {\n throw new ApiError(\n \"Sua assinatura está vencida. Assine um novo plano para continuar.\",\n \"SUBSCRIPTION_EXPIRED\",\n 403,\n );\n }\n});\n"],"mappings":"AAAA,OAAO;AAEP,SAAS,aAAa;AAEtB,SAAS,gBAAgB;AACzB,SAAS,+BAA+B;AACxC,SAAS,iCAAiC;AAWnC,MAAM,+BAA+B,MAAM,YAAY;AAC5D,QAAM,SAAS,MAAM,wBAAwB,EAAE,QAAQ,KAAK,CAAC;AAE7D,MAAI,CAAC,OAAO,SAAS;AACnB,UAAM,IAAI;AAAA,MACR,OAAO,SAAS;AAAA,MAChB;AAAA,MACA;AAAA,IACF;AAAA,EACF;AAEA,QAAM,eAAe,OAAO,MAAM,OAAO,CAAC;AAE1C,MAAI,CAAC,cAAc;AACjB,UAAM,IAAI;AAAA,MACR;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,EACF;AAEA,QAAM,aAAa,0BAA0B,YAAY;AACzD,QAAM,YAAY,CAAC,CAAC,cAAc,KAAK,IAAI,KAAK,WAAW,QAAQ;AAEnE,MAAI,WAAW;AACb,UAAM,IAAI;AAAA,MACR;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,EACF;AACF,CAAC;","names":[]}
|
|
@@ -11,7 +11,7 @@ function useCards(options = {}) {
|
|
|
11
11
|
queryFn: ({ signal }) => apiFetch("/api/cards", { signal }),
|
|
12
12
|
select: (result) => ({
|
|
13
13
|
...result,
|
|
14
|
-
data: result.data.filter((card) => !card.deleted && (includeVindi || !card.is_vindi))
|
|
14
|
+
data: result.data.filter((card) => !card.deleted && (includeVindi || !card.is_vindi)).sort((a, b) => Number(b.is_default) - Number(a.is_default))
|
|
15
15
|
}),
|
|
16
16
|
enabled: options.enabled ?? true
|
|
17
17
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/modules/cards/hooks/cards.hook.ts"],"sourcesContent":["'use client';\n\nimport { useQuery } from '@tanstack/react-query';\nimport { apiFetch } from '../../../infra/http/api-fetch';\nimport type { PaginatedSuccessResult } from '../../../infra/api/types';\nimport type { Card } from '../types';\nimport { useAuthQueryKey } from '../../../hooks/useAuthQueryKey';\n\nexport const CARDS_QUERY_KEY = ['cards'] as const;\n\nexport interface UseCardsOptions {\n enabled?: boolean;\n /* Inclui cartões Vindi (is_vindi) na lista. Padrão false: cartão Vindi não é cobrável\n * pela Stripe, então superfícies de pagamento (checkout, pagar cobrança, troca de\n * plano) ficam seguras por omissão — só telas de exibição/gestão fazem opt-in. */\n includeVindi?: boolean;\n}\n\nexport function useCards(options: UseCardsOptions = {}) {\n const queryKey = useAuthQueryKey([...CARDS_QUERY_KEY]);\n const includeVindi = options.includeVindi ?? false;\n\n return useQuery({\n queryKey,\n queryFn: ({ signal }) => apiFetch<PaginatedSuccessResult<Card>>('/api/cards', { signal }),\n select: (result) => ({\n ...result,\n data: result.data.filter((card) => !card.deleted && (includeVindi || !card.is_vindi)),\n }),\n enabled: options.enabled ?? true,\n });\n}\n"],"mappings":";AAEA,SAAS,gBAAgB;AACzB,SAAS,gBAAgB;AAGzB,SAAS,uBAAuB;AAEzB,MAAM,kBAAkB,CAAC,OAAO;AAUhC,SAAS,SAAS,UAA2B,CAAC,GAAG;AACtD,QAAM,WAAW,gBAAgB,CAAC,GAAG,eAAe,CAAC;AACrD,QAAM,eAAe,QAAQ,gBAAgB;AAE7C,SAAO,SAAS;AAAA,IACd;AAAA,IACA,SAAS,CAAC,EAAE,OAAO,MAAM,SAAuC,cAAc,EAAE,OAAO,CAAC;AAAA,IACxF,QAAQ,CAAC,YAAY;AAAA,MACnB,GAAG;AAAA,MACH,MAAM,OAAO,
|
|
1
|
+
{"version":3,"sources":["../../../../src/modules/cards/hooks/cards.hook.ts"],"sourcesContent":["'use client';\n\nimport { useQuery } from '@tanstack/react-query';\nimport { apiFetch } from '../../../infra/http/api-fetch';\nimport type { PaginatedSuccessResult } from '../../../infra/api/types';\nimport type { Card } from '../types';\nimport { useAuthQueryKey } from '../../../hooks/useAuthQueryKey';\n\nexport const CARDS_QUERY_KEY = ['cards'] as const;\n\nexport interface UseCardsOptions {\n enabled?: boolean;\n /* Inclui cartões Vindi (is_vindi) na lista. Padrão false: cartão Vindi não é cobrável\n * pela Stripe, então superfícies de pagamento (checkout, pagar cobrança, troca de\n * plano) ficam seguras por omissão — só telas de exibição/gestão fazem opt-in. */\n includeVindi?: boolean;\n}\n\nexport function useCards(options: UseCardsOptions = {}) {\n const queryKey = useAuthQueryKey([...CARDS_QUERY_KEY]);\n const includeVindi = options.includeVindi ?? false;\n\n return useQuery({\n queryKey,\n queryFn: ({ signal }) => apiFetch<PaginatedSuccessResult<Card>>('/api/cards', { signal }),\n select: (result) => ({\n ...result,\n data: result.data\n .filter((card) => !card.deleted && (includeVindi || !card.is_vindi))\n .sort((a, b) => Number(b.is_default) - Number(a.is_default)),\n }),\n enabled: options.enabled ?? true,\n });\n}\n"],"mappings":";AAEA,SAAS,gBAAgB;AACzB,SAAS,gBAAgB;AAGzB,SAAS,uBAAuB;AAEzB,MAAM,kBAAkB,CAAC,OAAO;AAUhC,SAAS,SAAS,UAA2B,CAAC,GAAG;AACtD,QAAM,WAAW,gBAAgB,CAAC,GAAG,eAAe,CAAC;AACrD,QAAM,eAAe,QAAQ,gBAAgB;AAE7C,SAAO,SAAS;AAAA,IACd;AAAA,IACA,SAAS,CAAC,EAAE,OAAO,MAAM,SAAuC,cAAc,EAAE,OAAO,CAAC;AAAA,IACxF,QAAQ,CAAC,YAAY;AAAA,MACnB,GAAG;AAAA,MACH,MAAM,OAAO,KACV,OAAO,CAAC,SAAS,CAAC,KAAK,YAAY,gBAAgB,CAAC,KAAK,SAAS,EAClE,KAAK,CAAC,GAAG,MAAM,OAAO,EAAE,UAAU,IAAI,OAAO,EAAE,UAAU,CAAC;AAAA,IAC/D;AAAA,IACA,SAAS,QAAQ,WAAW;AAAA,EAC9B,CAAC;AACH;","names":[]}
|
|
@@ -11,7 +11,8 @@ function useHandleDeleteCard({
|
|
|
11
11
|
const activeSubscription = subscriptionData?.data?.[0] ?? null;
|
|
12
12
|
return useCallback(
|
|
13
13
|
(card) => {
|
|
14
|
-
const
|
|
14
|
+
const subscriptionCardId = activeSubscription?.id_card?.toString() ?? "";
|
|
15
|
+
const isCardLinkedToSubscription = !!activeSubscription && activeSubscription.active === true && !activeSubscription.date_cancellation && !hasSubscriptionExpired(activeSubscription.date_due) && (card.id.toString() === subscriptionCardId || /^\d+$/.test(subscriptionCardId) && card.is_default && !card.is_vindi);
|
|
15
16
|
if (isCardLinkedToSubscription) {
|
|
16
17
|
openModal("cannotDeleteCardModal", {
|
|
17
18
|
cardId: card.id.toString()
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/modules/cards/hooks/handle-delete-card.hook.ts"],"sourcesContent":["'use client';\n\nimport { useCallback } from 'react';\nimport { useModalManager } from '../../../store/useModalManager';\nimport { useActiveSubscription } from '../../subscriptions/hooks/find-active-subscription.hook';\nimport { hasSubscriptionExpired } from '../../subscriptions/utils/has-subscription-expired';\nimport type { Card } from '../types';\n\ntype UseHandleDeleteCardOptions = {\n onAfterDelete?: (card: Card) => void;\n};\n\nexport function useHandleDeleteCard({\n onAfterDelete,\n}: UseHandleDeleteCardOptions = {}) {\n const { openModal } = useModalManager();\n const { data: subscriptionData } = useActiveSubscription();\n const activeSubscription = subscriptionData?.data?.[0] ?? null;\n\n return useCallback(\n (card: Card) => {\n // Só bloqueia a exclusão quando o cartão é o método de pagamento de uma\n // assinatura REALMENTE ativa (mesma definição de ConfigurationsMyAccountModal).\n // Se a assinatura foi cancelada ou já expirou, ela não está mais ativa —\n // pula a verificação e segue direto pra exclusão do cartão.\n const isCardLinkedToSubscription =\n !!activeSubscription &&\n activeSubscription.active === true &&\n !activeSubscription.date_cancellation &&\n !hasSubscriptionExpired(activeSubscription.date_due) &&\n card.id.toString() ===
|
|
1
|
+
{"version":3,"sources":["../../../../src/modules/cards/hooks/handle-delete-card.hook.ts"],"sourcesContent":["'use client';\n\nimport { useCallback } from 'react';\nimport { useModalManager } from '../../../store/useModalManager';\nimport { useActiveSubscription } from '../../subscriptions/hooks/find-active-subscription.hook';\nimport { hasSubscriptionExpired } from '../../subscriptions/utils/has-subscription-expired';\nimport type { Card } from '../types';\n\ntype UseHandleDeleteCardOptions = {\n onAfterDelete?: (card: Card) => void;\n};\n\nexport function useHandleDeleteCard({\n onAfterDelete,\n}: UseHandleDeleteCardOptions = {}) {\n const { openModal } = useModalManager();\n const { data: subscriptionData } = useActiveSubscription();\n const activeSubscription = subscriptionData?.data?.[0] ?? null;\n\n return useCallback(\n (card: Card) => {\n // Só bloqueia a exclusão quando o cartão é o método de pagamento de uma\n // assinatura REALMENTE ativa (mesma definição de ConfigurationsMyAccountModal).\n // Se a assinatura foi cancelada ou já expirou, ela não está mais ativa —\n // pula a verificação e segue direto pra exclusão do cartão.\n const subscriptionCardId = activeSubscription?.id_card?.toString() ?? '';\n const isCardLinkedToSubscription =\n !!activeSubscription &&\n activeSubscription.active === true &&\n !activeSubscription.date_cancellation &&\n !hasSubscriptionExpired(activeSubscription.date_due) &&\n (card.id.toString() === subscriptionCardId ||\n (/^\\d+$/.test(subscriptionCardId) && card.is_default && !card.is_vindi));\n\n if (isCardLinkedToSubscription) {\n openModal('cannotDeleteCardModal', {\n cardId: card.id.toString(),\n });\n return;\n }\n\n openModal('deleteCardModal', {\n cardId: card.id.toString(),\n ...(onAfterDelete && { onSuccess: () => onAfterDelete(card) }),\n });\n },\n [activeSubscription, openModal, onAfterDelete],\n );\n}\n"],"mappings":";AAEA,SAAS,mBAAmB;AAC5B,SAAS,uBAAuB;AAChC,SAAS,6BAA6B;AACtC,SAAS,8BAA8B;AAOhC,SAAS,oBAAoB;AAAA,EAClC;AACF,IAAgC,CAAC,GAAG;AAClC,QAAM,EAAE,UAAU,IAAI,gBAAgB;AACtC,QAAM,EAAE,MAAM,iBAAiB,IAAI,sBAAsB;AACzD,QAAM,qBAAqB,kBAAkB,OAAO,CAAC,KAAK;AAE1D,SAAO;AAAA,IACL,CAAC,SAAe;AAKd,YAAM,qBAAqB,oBAAoB,SAAS,SAAS,KAAK;AACtE,YAAM,6BACJ,CAAC,CAAC,sBACF,mBAAmB,WAAW,QAC9B,CAAC,mBAAmB,qBACpB,CAAC,uBAAuB,mBAAmB,QAAQ,MAClD,KAAK,GAAG,SAAS,MAAM,sBACrB,QAAQ,KAAK,kBAAkB,KAAK,KAAK,cAAc,CAAC,KAAK;AAElE,UAAI,4BAA4B;AAC9B,kBAAU,yBAAyB;AAAA,UACjC,QAAQ,KAAK,GAAG,SAAS;AAAA,QAC3B,CAAC;AACD;AAAA,MACF;AAEA,gBAAU,mBAAmB;AAAA,QAC3B,QAAQ,KAAK,GAAG,SAAS;AAAA,QACzB,GAAI,iBAAiB,EAAE,WAAW,MAAM,cAAc,IAAI,EAAE;AAAA,MAC9D,CAAC;AAAA,IACH;AAAA,IACA,CAAC,oBAAoB,WAAW,aAAa;AAAA,EAC/C;AACF;","names":[]}
|
package/package.json
CHANGED
|
@@ -33,11 +33,12 @@ export function CannotDeleteCardModal() {
|
|
|
33
33
|
async (card: Card) => {
|
|
34
34
|
if (!activeSubscription || !cardToDelete) return;
|
|
35
35
|
|
|
36
|
-
/* Assinatura
|
|
37
|
-
*
|
|
38
|
-
*
|
|
36
|
+
/* Assinatura no trilho Vindi (id_card numérico) trocando pra um cartão Stripe:
|
|
37
|
+
* enviar payment_method_id (pm_) dispara a migração Vindi→Stripe no backend —
|
|
38
|
+
* id_card com pm_ cairia no fluxo Vindi, que só aceita id numérico.
|
|
39
39
|
* Nos demais casos (Vindi→Vindi, Stripe→Stripe) o id_card atual já resolve. */
|
|
40
|
-
const
|
|
40
|
+
const subscriptionCardId = activeSubscription.id_card?.toString() ?? '';
|
|
41
|
+
const migratesVindiToStripe = /^\d+$/.test(subscriptionCardId) && !card.is_vindi;
|
|
41
42
|
|
|
42
43
|
try {
|
|
43
44
|
await updatePayment({
|
|
@@ -47,7 +47,7 @@ export function PaymentInfoCard({
|
|
|
47
47
|
const hasCards = cards.length > 0;
|
|
48
48
|
|
|
49
49
|
const hasMoreThan3Cards = cards.length > 3;
|
|
50
|
-
const shouldShowManageButton = isMobile && hasMoreThan3Cards;
|
|
50
|
+
const shouldShowManageButton = isMobile && hasMoreThan3Cards && !!onManageCards;
|
|
51
51
|
const displayedCards = shouldShowManageButton ? cards.slice(0, 3) : cards;
|
|
52
52
|
|
|
53
53
|
const handleOpenAddCardModal = useCallback(() => {
|
|
@@ -1,12 +1,24 @@
|
|
|
1
1
|
import 'server-only';
|
|
2
2
|
|
|
3
|
+
import { cache } from 'react';
|
|
4
|
+
|
|
3
5
|
import { UserProfile } from '../../users/schema';
|
|
4
6
|
import { findUserById } from '../../users/action/find-user-by-id.action';
|
|
5
7
|
import { ApiError } from '../../../infra/api/types';
|
|
6
8
|
import { assertManagementSubscription } from './assert-management-subscription';
|
|
7
9
|
import type { ManagementPermission } from '../types/management-permission.type';
|
|
8
10
|
|
|
9
|
-
|
|
11
|
+
/**
|
|
12
|
+
* Perfil do usuário autenticado, deduplicado por request via React.cache — mesma razão
|
|
13
|
+
* do `requireValidSession`.
|
|
14
|
+
*
|
|
15
|
+
* Este gate abre TODA leitura e escrita gateada, e o perfil não depende da permissão que
|
|
16
|
+
* está sendo checada: uma request que toca N services pagava N round-trips ao backend para
|
|
17
|
+
* reler a mesma linha. Medido em produção (26/08, carregamento da aba de assinatura de uma
|
|
18
|
+
* conta): ~30 GETs de `/accounts/{id}/users/{id}` em 20 s, 200–880 ms cada, competindo pelo
|
|
19
|
+
* mesmo worker de banco que as chamadas de verdade.
|
|
20
|
+
*/
|
|
21
|
+
const getCurrentUserProfile = cache(async () => {
|
|
10
22
|
const result = await findUserById();
|
|
11
23
|
|
|
12
24
|
if (!result.success || !result.data) {
|
|
@@ -18,7 +30,7 @@ async function getCurrentUserProfile() {
|
|
|
18
30
|
}
|
|
19
31
|
|
|
20
32
|
return result.data.profile;
|
|
21
|
-
}
|
|
33
|
+
});
|
|
22
34
|
|
|
23
35
|
export async function assertManagementPermission(permission: ManagementPermission) {
|
|
24
36
|
const profile = await getCurrentUserProfile();
|
|
@@ -1,10 +1,21 @@
|
|
|
1
1
|
import "server-only";
|
|
2
2
|
|
|
3
|
+
import { cache } from "react";
|
|
4
|
+
|
|
3
5
|
import { ApiError } from "../../../infra/api/types";
|
|
4
6
|
import { listSubscriptionsAction } from "../../subscriptions/actions/list-subscriptions.action";
|
|
5
7
|
import { getSubscriptionExpiryDate } from "../../subscriptions/utils/get-subscription-expiry-date";
|
|
6
8
|
|
|
7
|
-
|
|
9
|
+
/**
|
|
10
|
+
* Deduplicado por request via React.cache, como `getCurrentUserProfile` e `requireValidSession`.
|
|
11
|
+
*
|
|
12
|
+
* A leitura por trás é `/accounts/{id}/subscriptions?limit=1`, e ela é MAIS cara que o gate de
|
|
13
|
+
* perfil: para conta Stripe a listagem consulta a API do gateway ao vivo. Em produção (26/08) o
|
|
14
|
+
* mesmo carregamento disparou quatro dessas em paralelo — 727 ms, 4,2 s, 5,0 s e 7,0 s, contra um
|
|
15
|
+
* p50 de 528 ms para o endpoint. Elas se atrasam entre si: são cópias da mesma pergunta disputando
|
|
16
|
+
* a mesma fila.
|
|
17
|
+
*/
|
|
18
|
+
export const assertManagementSubscription = cache(async () => {
|
|
8
19
|
const result = await listSubscriptionsAction({ active: true });
|
|
9
20
|
|
|
10
21
|
if (!result.success) {
|
|
@@ -35,4 +46,4 @@ export async function assertManagementSubscription() {
|
|
|
35
46
|
403,
|
|
36
47
|
);
|
|
37
48
|
}
|
|
38
|
-
}
|
|
49
|
+
});
|
|
@@ -25,7 +25,9 @@ export function useCards(options: UseCardsOptions = {}) {
|
|
|
25
25
|
queryFn: ({ signal }) => apiFetch<PaginatedSuccessResult<Card>>('/api/cards', { signal }),
|
|
26
26
|
select: (result) => ({
|
|
27
27
|
...result,
|
|
28
|
-
data: result.data
|
|
28
|
+
data: result.data
|
|
29
|
+
.filter((card) => !card.deleted && (includeVindi || !card.is_vindi))
|
|
30
|
+
.sort((a, b) => Number(b.is_default) - Number(a.is_default)),
|
|
29
31
|
}),
|
|
30
32
|
enabled: options.enabled ?? true,
|
|
31
33
|
});
|
|
@@ -23,12 +23,14 @@ export function useHandleDeleteCard({
|
|
|
23
23
|
// assinatura REALMENTE ativa (mesma definição de ConfigurationsMyAccountModal).
|
|
24
24
|
// Se a assinatura foi cancelada ou já expirou, ela não está mais ativa —
|
|
25
25
|
// pula a verificação e segue direto pra exclusão do cartão.
|
|
26
|
+
const subscriptionCardId = activeSubscription?.id_card?.toString() ?? '';
|
|
26
27
|
const isCardLinkedToSubscription =
|
|
27
28
|
!!activeSubscription &&
|
|
28
29
|
activeSubscription.active === true &&
|
|
29
30
|
!activeSubscription.date_cancellation &&
|
|
30
31
|
!hasSubscriptionExpired(activeSubscription.date_due) &&
|
|
31
|
-
card.id.toString() ===
|
|
32
|
+
(card.id.toString() === subscriptionCardId ||
|
|
33
|
+
(/^\d+$/.test(subscriptionCardId) && card.is_default && !card.is_vindi));
|
|
32
34
|
|
|
33
35
|
if (isCardLinkedToSubscription) {
|
|
34
36
|
openModal('cannotDeleteCardModal', {
|