@tribe-nest/forge 3.54.0 → 3.57.0
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/package.json +1 -1
- package/src/data/queries/useCoachingProducts.ts +15 -1
- package/src/data/queries/useCourses.ts +15 -1
- package/src/data/queries/useEvents.ts +12 -1
- package/src/data/queries/useMembership.ts +176 -4
- package/src/data/queries/useProducts.ts +15 -1
- package/src/data/queries/useSubscriptions.ts +21 -1
- package/src/i18n/de.json +74 -0
- package/src/i18n/en.json +74 -0
- package/src/server/index.ts +74 -0
- package/src/server/platformEvents.generated.ts +60 -0
- package/src/types/models.ts +127 -0
- package/src/ui/headless/index.ts +23 -0
- package/src/ui/headless/membership/_tests/membershipApplication.spec.ts +85 -0
- package/src/ui/headless/membership/_tests/membershipCheckoutRefetch.spec.tsx +2 -0
- package/src/ui/headless/membership/_tests/membershipTrial.spec.ts +61 -0
- package/src/ui/headless/membership/_tests/useMembershipCheckoutApplication.spec.tsx +253 -0
- package/src/ui/headless/membership/_tests/useMembershipCheckoutQuestionnaire.spec.tsx +241 -0
- package/src/ui/headless/membership/_tests/useMembershipCheckoutTrial.spec.tsx +165 -0
- package/src/ui/headless/membership/membershipApplication.ts +109 -0
- package/src/ui/headless/membership/membershipQuestionnaire.ts +44 -0
- package/src/ui/headless/membership/membershipTrial.ts +53 -0
- package/src/ui/headless/membership/useMembershipCheckout.ts +227 -8
- package/src/ui/index.ts +9 -0
- package/src/ui/payment/ForgePaymentProvider.tsx +14 -0
- package/src/ui/payment/ForgeStripePayment.tsx +78 -7
- package/src/ui/payment/_tests/ForgeStripePaymentTrial.spec.tsx +101 -0
- package/src/ui/payment/_tests/stripeIntentKind.spec.ts +43 -0
- package/src/ui/payment/stripeIntentKind.ts +32 -0
- package/src/ui/styled/AccountDashboard.tsx +160 -2
- package/src/ui/styled/LoginForm.tsx +5 -3
- package/src/ui/styled/MembershipCheckout.tsx +596 -256
- package/src/ui/styled/MembershipTierCallout.tsx +17 -3
- package/src/ui/styled/MembershipTiers.tsx +148 -6
- package/src/ui/styled/ProductGrid.tsx +14 -2
- package/src/ui/styled/SignupForm.tsx +4 -2
- package/src/ui/styled/_tests/AccountDashboardTrial.spec.tsx +105 -0
- package/src/ui/styled/_tests/MembershipCheckout.spec.tsx +106 -0
- package/src/ui/styled/_tests/membershipTiersCuratedAccess.spec.tsx +139 -0
- package/src/ui/styled/_tests/membershipTiersTrial.spec.tsx +97 -0
package/package.json
CHANGED
|
@@ -7,7 +7,20 @@ export interface GetCoachingProductsParams {
|
|
|
7
7
|
query?: string;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
export function useGetCoachingProducts(
|
|
10
|
+
export function useGetCoachingProducts(
|
|
11
|
+
params?: GetCoachingProductsParams,
|
|
12
|
+
enabled = true,
|
|
13
|
+
options?: {
|
|
14
|
+
/**
|
|
15
|
+
* The page a server render already fetched.
|
|
16
|
+
*
|
|
17
|
+
* Seeds the cache so the first client render matches the markup and nothing
|
|
18
|
+
* is refetched. Must be built with the SAME params as the key below, or the
|
|
19
|
+
* seed belongs to a different query and is ignored.
|
|
20
|
+
*/
|
|
21
|
+
initialData?: PaginatedData<CoachingProduct>;
|
|
22
|
+
},
|
|
23
|
+
) {
|
|
11
24
|
const { client, profileId } = useForge();
|
|
12
25
|
|
|
13
26
|
return useQuery<PaginatedData<CoachingProduct>>({
|
|
@@ -23,6 +36,7 @@ export function useGetCoachingProducts(params?: GetCoachingProductsParams, enabl
|
|
|
23
36
|
return res.data;
|
|
24
37
|
},
|
|
25
38
|
enabled: !!profileId && !!client && enabled,
|
|
39
|
+
initialData: options?.initialData,
|
|
26
40
|
});
|
|
27
41
|
}
|
|
28
42
|
|
|
@@ -90,7 +90,20 @@ export interface GetCoursesParams {
|
|
|
90
90
|
query?: string;
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
export function useGetCourses(
|
|
93
|
+
export function useGetCourses(
|
|
94
|
+
params?: GetCoursesParams,
|
|
95
|
+
enabled = true,
|
|
96
|
+
options?: {
|
|
97
|
+
/**
|
|
98
|
+
* The page a server render already fetched.
|
|
99
|
+
*
|
|
100
|
+
* Seeds the cache so the first client render matches the markup and nothing
|
|
101
|
+
* is refetched. Must be built with the SAME params as the key below, or the
|
|
102
|
+
* seed belongs to a different query and is ignored.
|
|
103
|
+
*/
|
|
104
|
+
initialData?: PaginatedData<PublicCourse>;
|
|
105
|
+
},
|
|
106
|
+
) {
|
|
94
107
|
const { client, profileId } = useForge();
|
|
95
108
|
|
|
96
109
|
return useQuery<PaginatedData<PublicCourse>>({
|
|
@@ -106,6 +119,7 @@ export function useGetCourses(params?: GetCoursesParams, enabled = true) {
|
|
|
106
119
|
return res.data;
|
|
107
120
|
},
|
|
108
121
|
enabled: !!profileId && !!client && enabled,
|
|
122
|
+
initialData: options?.initialData,
|
|
109
123
|
});
|
|
110
124
|
}
|
|
111
125
|
|
|
@@ -3,7 +3,17 @@ import { useForge } from "../../provider/ForgeProvider";
|
|
|
3
3
|
import { useMutation, useQuery } from "@tanstack/react-query";
|
|
4
4
|
|
|
5
5
|
/** All published events for the profile. */
|
|
6
|
-
export function useEvents(
|
|
6
|
+
export function useEvents(options?: {
|
|
7
|
+
/**
|
|
8
|
+
* The list a server render already fetched (`fetchEventsServer`).
|
|
9
|
+
*
|
|
10
|
+
* Seeds the cache so the first client render shows the same events as the
|
|
11
|
+
* markup and nothing is refetched. Without it the list page renders a
|
|
12
|
+
* spinner on the server, which is what a crawler sees and what a fan sees
|
|
13
|
+
* for as long as the request takes.
|
|
14
|
+
*/
|
|
15
|
+
initialData?: IEvent[];
|
|
16
|
+
}) {
|
|
7
17
|
const { client, profileId } = useForge();
|
|
8
18
|
|
|
9
19
|
return useQuery<IEvent[]>({
|
|
@@ -15,6 +25,7 @@ export function useEvents() {
|
|
|
15
25
|
return res.data;
|
|
16
26
|
},
|
|
17
27
|
enabled: !!profileId && !!client,
|
|
28
|
+
initialData: options?.initialData,
|
|
18
29
|
});
|
|
19
30
|
}
|
|
20
31
|
|
|
@@ -1,22 +1,60 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
ApplyForMembershipResult,
|
|
3
|
+
MembershipApplication,
|
|
4
|
+
MembershipInviteInfo,
|
|
5
|
+
MembershipTier,
|
|
6
|
+
QuestionnaireAnswer,
|
|
7
|
+
} from "../../types/models";
|
|
2
8
|
import { useForge } from "../../provider/ForgeProvider";
|
|
3
|
-
import { useQuery } from "@tanstack/react-query";
|
|
9
|
+
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The membership invite token this visitor arrived holding, read from the
|
|
13
|
+
* `invite` search param of the CURRENT url (the invite email links to
|
|
14
|
+
* `/i/membership/checkout?membershipTierId=X&invite=TOKEN`).
|
|
15
|
+
*
|
|
16
|
+
* Deliberately not stored anywhere: it is a bearer credential, and the durable
|
|
17
|
+
* copy is the email. Returns undefined on the server and when absent.
|
|
18
|
+
*/
|
|
19
|
+
export const readMembershipInviteFromUrl = (search?: string): string | undefined => {
|
|
20
|
+
const raw = search ?? (typeof window === "undefined" ? "" : window.location.search);
|
|
21
|
+
if (!raw) return undefined;
|
|
22
|
+
const token = new URLSearchParams(raw).get("invite")?.trim();
|
|
23
|
+
return token ? token : undefined;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export interface UseGetMembershipTiersOptions {
|
|
27
|
+
enabled?: boolean;
|
|
28
|
+
/**
|
|
29
|
+
* An invite token to forward, so an invite-only tier it unlocks is listed.
|
|
30
|
+
* Omitted (undefined), the `invite` search param of the current url is used;
|
|
31
|
+
* `null` forwards nothing even when the url carries one.
|
|
32
|
+
*/
|
|
33
|
+
inviteToken?: string | null;
|
|
34
|
+
}
|
|
4
35
|
|
|
5
36
|
/**
|
|
6
37
|
* `opts.enabled` gates the request on top of the usual profile/client checks —
|
|
7
38
|
* for a caller that only needs tier NAMES when something is actually gated, and
|
|
8
39
|
* must not add a request to every ungated storefront. Defaults to on, so every
|
|
9
40
|
* existing caller is unchanged.
|
|
41
|
+
*
|
|
42
|
+
* Invite-only tiers are omitted by the server unless the request carries a
|
|
43
|
+
* live invite token for them, so the token is part of the query key.
|
|
10
44
|
*/
|
|
11
|
-
export function useGetMembershipTiers(opts:
|
|
45
|
+
export function useGetMembershipTiers(opts: UseGetMembershipTiersOptions = {}) {
|
|
12
46
|
const { client, profileId } = useForge();
|
|
47
|
+
const inviteToken = opts.inviteToken === undefined ? readMembershipInviteFromUrl() : (opts.inviteToken ?? undefined);
|
|
13
48
|
|
|
14
49
|
return useQuery<MembershipTier[]>({
|
|
15
|
-
|
|
50
|
+
// The token is appended only when present, so the no-invite key stays the
|
|
51
|
+
// one every existing cache seed (SSR, specs) already writes.
|
|
52
|
+
queryKey: inviteToken ? ["membership-tiers", profileId, inviteToken] : ["membership-tiers", profileId],
|
|
16
53
|
queryFn: async () => {
|
|
17
54
|
const res = await client.get("/public/membership-tiers", {
|
|
18
55
|
params: {
|
|
19
56
|
profileId: profileId,
|
|
57
|
+
...(inviteToken ? { inviteToken } : {}),
|
|
20
58
|
},
|
|
21
59
|
});
|
|
22
60
|
return res.data;
|
|
@@ -24,3 +62,137 @@ export function useGetMembershipTiers(opts: { enabled?: boolean } = {}) {
|
|
|
24
62
|
enabled: !!profileId && !!client && opts.enabled !== false,
|
|
25
63
|
});
|
|
26
64
|
}
|
|
65
|
+
|
|
66
|
+
// ---- Invites -------------------------------------------------------------------
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* What a membership invite link is for: the tier and the invited email, so the
|
|
70
|
+
* landing page can name both before asking for anything.
|
|
71
|
+
*
|
|
72
|
+
* A dead token (unknown, expired, revoked or already accepted) is a 404, which
|
|
73
|
+
* is an ANSWER rather than a transient failure, so it is not retried. Tell the
|
|
74
|
+
* two apart with `isMembershipInviteDead(error)`.
|
|
75
|
+
*/
|
|
76
|
+
export function useMembershipInviteInfo(token?: string | null) {
|
|
77
|
+
const { client } = useForge();
|
|
78
|
+
|
|
79
|
+
return useQuery<MembershipInviteInfo>({
|
|
80
|
+
queryKey: ["membership-invite-info", token ?? null],
|
|
81
|
+
queryFn: async () => {
|
|
82
|
+
const res = await client.get("/public/membership-invites/info", { params: { token } });
|
|
83
|
+
return res.data;
|
|
84
|
+
},
|
|
85
|
+
enabled: !!token && !!client,
|
|
86
|
+
retry: (count, error) => !isMembershipInviteDead(error) && count < 2,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** True when an invite-info error means the invite is gone (404), not that the request failed. */
|
|
91
|
+
export function isMembershipInviteDead(error: unknown): boolean {
|
|
92
|
+
return (error as { response?: { status?: number } } | null)?.response?.status === 404;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ---- Applications ----------------------------------------------------------------
|
|
96
|
+
|
|
97
|
+
export type ApplyForMembershipInput = {
|
|
98
|
+
membershipTierId: string;
|
|
99
|
+
/** Required on a paid tier. */
|
|
100
|
+
billingCycle?: "month" | "year";
|
|
101
|
+
/** Required on a paid tier; same floor/ceiling rules as a subscription. */
|
|
102
|
+
amount?: number;
|
|
103
|
+
/**
|
|
104
|
+
* A free-text note, up to 1000 characters. Still accepted by the server, but
|
|
105
|
+
* no current checkout sends it: the tier's questions replaced it.
|
|
106
|
+
*/
|
|
107
|
+
message?: string;
|
|
108
|
+
/** Answers to the tier's questions. Required by the server when the tier has any. */
|
|
109
|
+
questionnaire?: QuestionnaireAnswer[];
|
|
110
|
+
/** Where Stripe returns after a redirect-based card confirmation. */
|
|
111
|
+
returnUrl?: string;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
/** Invalidate everything an application changes: the tier cards and the fan's list. */
|
|
115
|
+
function useInvalidateApplications() {
|
|
116
|
+
const queryClient = useQueryClient();
|
|
117
|
+
return () =>
|
|
118
|
+
Promise.all([
|
|
119
|
+
queryClient.invalidateQueries({ queryKey: ["membership-tiers"] }),
|
|
120
|
+
queryClient.invalidateQueries({ queryKey: ["my-membership-applications"] }),
|
|
121
|
+
]);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Apply to join an `application` tier.
|
|
126
|
+
*
|
|
127
|
+
* A free tier comes back `pending` with nothing more to do. A paid tier comes
|
|
128
|
+
* back `card_pending` with `requiresCard` and a SetupIntent `clientSecret`: the
|
|
129
|
+
* card is saved, not charged, and after the renderer confirms it the caller
|
|
130
|
+
* calls `useConfirmMembershipApplication`. Applying again while one is open
|
|
131
|
+
* returns that one.
|
|
132
|
+
*/
|
|
133
|
+
export function useApplyForMembership() {
|
|
134
|
+
const { client, profileId } = useForge();
|
|
135
|
+
const invalidate = useInvalidateApplications();
|
|
136
|
+
|
|
137
|
+
return useMutation<ApplyForMembershipResult, unknown, ApplyForMembershipInput>({
|
|
138
|
+
mutationFn: async (body) => {
|
|
139
|
+
const res = await client.post("/public/membership-applications", { ...body, profileId });
|
|
140
|
+
return res.data;
|
|
141
|
+
},
|
|
142
|
+
onSuccess: () => void invalidate(),
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Tell the server the card step finished. Idempotent. The application comes
|
|
148
|
+
* back `pending` once the provider shows the card saved, still `card_pending`
|
|
149
|
+
* if it does not yet, or `withdrawn` with `closedReason: "tier_full"` when the
|
|
150
|
+
* last seat went in the meantime.
|
|
151
|
+
*/
|
|
152
|
+
export function useConfirmMembershipApplication() {
|
|
153
|
+
const { client, profileId } = useForge();
|
|
154
|
+
const invalidate = useInvalidateApplications();
|
|
155
|
+
|
|
156
|
+
return useMutation<{ application: MembershipApplication }, unknown, { applicationId: string }>({
|
|
157
|
+
mutationFn: async ({ applicationId }) => {
|
|
158
|
+
const res = await client.post(`/public/membership-applications/${encodeURIComponent(applicationId)}/confirm`, {
|
|
159
|
+
profileId,
|
|
160
|
+
});
|
|
161
|
+
return res.data;
|
|
162
|
+
},
|
|
163
|
+
onSuccess: () => void invalidate(),
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Withdraw an application that is still `card_pending` or `pending`. */
|
|
168
|
+
export function useWithdrawMembershipApplication() {
|
|
169
|
+
const { client, profileId } = useForge();
|
|
170
|
+
const invalidate = useInvalidateApplications();
|
|
171
|
+
|
|
172
|
+
return useMutation<{ application: MembershipApplication }, unknown, { applicationId: string }>({
|
|
173
|
+
mutationFn: async ({ applicationId }) => {
|
|
174
|
+
const res = await client.post(`/public/membership-applications/${encodeURIComponent(applicationId)}/withdraw`, {
|
|
175
|
+
profileId,
|
|
176
|
+
});
|
|
177
|
+
return res.data;
|
|
178
|
+
},
|
|
179
|
+
onSuccess: () => void invalidate(),
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* The signed-in fan's applications on this profile, newest first. Pass the
|
|
185
|
+
* account id so the query waits for sign-in and is keyed per account.
|
|
186
|
+
*/
|
|
187
|
+
export function useMyMembershipApplications(accountId?: string) {
|
|
188
|
+
const { client, profileId } = useForge();
|
|
189
|
+
|
|
190
|
+
return useQuery<MembershipApplication[]>({
|
|
191
|
+
queryKey: ["my-membership-applications", accountId, profileId],
|
|
192
|
+
queryFn: async () => {
|
|
193
|
+
const res = await client.get("/public/membership-applications/mine", { params: { profileId } });
|
|
194
|
+
return res.data;
|
|
195
|
+
},
|
|
196
|
+
enabled: !!accountId && !!profileId && !!client,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
@@ -41,7 +41,20 @@ export function useFeaturedProducts(productType?: ProductType) {
|
|
|
41
41
|
});
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
export function useGetProducts(
|
|
44
|
+
export function useGetProducts(
|
|
45
|
+
params?: GetProductsParams,
|
|
46
|
+
enabled = true,
|
|
47
|
+
options?: {
|
|
48
|
+
/**
|
|
49
|
+
* The page a server render already fetched.
|
|
50
|
+
*
|
|
51
|
+
* Seeds the cache so the first client render matches the markup and nothing
|
|
52
|
+
* is refetched. Must be built with the SAME params as the key below, or the
|
|
53
|
+
* seed belongs to a different query and is ignored.
|
|
54
|
+
*/
|
|
55
|
+
initialData?: PaginatedData<IPublicProduct>;
|
|
56
|
+
},
|
|
57
|
+
) {
|
|
45
58
|
const { client, profileId } = useForge();
|
|
46
59
|
|
|
47
60
|
return useQuery<PaginatedData<IPublicProduct>>({
|
|
@@ -64,6 +77,7 @@ export function useGetProducts(params?: GetProductsParams, enabled = true) {
|
|
|
64
77
|
return res.data;
|
|
65
78
|
},
|
|
66
79
|
enabled: !!profileId && !!client && enabled,
|
|
80
|
+
initialData: options?.initialData,
|
|
67
81
|
});
|
|
68
82
|
}
|
|
69
83
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useForge } from "../../provider/ForgeProvider";
|
|
2
2
|
import { useMutation } from "@tanstack/react-query";
|
|
3
3
|
import { usePublicAuth } from "../../contexts/PublicAuthContext";
|
|
4
|
+
import type { QuestionnaireAnswer } from "../../types/models";
|
|
4
5
|
import {
|
|
5
6
|
getMembershipAccess,
|
|
6
7
|
getMembershipStatusMessage,
|
|
@@ -14,6 +15,13 @@ export type CreateSubscriptionInput = {
|
|
|
14
15
|
membershipTierId: string;
|
|
15
16
|
returnUrl: string;
|
|
16
17
|
attributionRefId?: string;
|
|
18
|
+
/**
|
|
19
|
+
* A membership invite token. Required by the server on an `invite` tier (the
|
|
20
|
+
* invite must be for the signed-in email); ignored on an open tier.
|
|
21
|
+
*/
|
|
22
|
+
inviteToken?: string;
|
|
23
|
+
/** Answers to the tier's questions. Required by the server when the tier has any. */
|
|
24
|
+
questionnaire?: QuestionnaireAnswer[];
|
|
17
25
|
};
|
|
18
26
|
|
|
19
27
|
/**
|
|
@@ -41,6 +49,12 @@ export type CreateSubscriptionResult = {
|
|
|
41
49
|
currentPeriodEnd?: string;
|
|
42
50
|
requiresPayment?: boolean;
|
|
43
51
|
changed?: boolean;
|
|
52
|
+
/**
|
|
53
|
+
* Set when this subscription starts with a free trial. `clientSecret` is then
|
|
54
|
+
* a Stripe SetupIntent secret (`seti_...`): the card is saved, nothing is
|
|
55
|
+
* charged today, and the first charge is on `endsAt`.
|
|
56
|
+
*/
|
|
57
|
+
trial?: { days: number; endsAt: string } | null;
|
|
44
58
|
};
|
|
45
59
|
|
|
46
60
|
/** Subscribe to a paid tier, or move an existing subscription onto another one. */
|
|
@@ -59,7 +73,13 @@ export function useCreateSubscription() {
|
|
|
59
73
|
export function useCreateFreeSubscription() {
|
|
60
74
|
const { client, profileId } = useForge();
|
|
61
75
|
|
|
62
|
-
return useMutation<unknown, unknown, {
|
|
76
|
+
return useMutation<unknown, unknown, {
|
|
77
|
+
membershipTierId: string;
|
|
78
|
+
attributionRefId?: string;
|
|
79
|
+
inviteToken?: string;
|
|
80
|
+
/** Answers to the tier's questions. Required by the server when the tier has any. */
|
|
81
|
+
questionnaire?: QuestionnaireAnswer[];
|
|
82
|
+
}>({
|
|
63
83
|
mutationFn: async (body) => {
|
|
64
84
|
const res = await client.post("/public/payments/subscriptions/free", { ...body, profileId });
|
|
65
85
|
return res.data;
|
package/src/i18n/de.json
CHANGED
|
@@ -1,4 +1,19 @@
|
|
|
1
1
|
{
|
|
2
|
+
"forge.account_dashboard.application_complete": "Mitgliedschaft abschließen",
|
|
3
|
+
"forge.account_dashboard.application_complete_by": "Abschließen bis {date}",
|
|
4
|
+
"forge.account_dashboard.application_status_approved": "Angenommen, wird eingerichtet",
|
|
5
|
+
"forge.account_dashboard.application_status_card_pending": "Karte noch nicht bestätigt",
|
|
6
|
+
"forge.account_dashboard.application_status_declined": "Nicht angenommen",
|
|
7
|
+
"forge.account_dashboard.application_status_expired": "Abgelaufen",
|
|
8
|
+
"forge.account_dashboard.application_status_joined": "Beigetreten",
|
|
9
|
+
"forge.account_dashboard.application_status_payment_failed": "Angenommen: Mitgliedschaft abschließen",
|
|
10
|
+
"forge.account_dashboard.application_status_pending": "Bewerbung wird geprüft",
|
|
11
|
+
"forge.account_dashboard.application_status_withdrawn": "Zurückgezogen",
|
|
12
|
+
"forge.account_dashboard.application_submitted_on": "Beworben am {date}",
|
|
13
|
+
"forge.account_dashboard.application_withdraw": "Zurückziehen",
|
|
14
|
+
"forge.account_dashboard.application_withdraw_error": "Wir konnten deine Bewerbung nicht zurückziehen. Bitte versuche es erneut.",
|
|
15
|
+
"forge.account_dashboard.application_withdrawing": "Wird zurückgezogen…",
|
|
16
|
+
"forge.account_dashboard.applications_title": "Deine Bewerbungen",
|
|
2
17
|
"forge.account_dashboard.billing_portal_error": "Wir konnten die Zahlungsseite nicht öffnen. Bitte kontaktiere uns.",
|
|
3
18
|
"forge.account_dashboard.billing_portal_opening": "Wird geöffnet…",
|
|
4
19
|
"forge.account_dashboard.booking_cancel_error": "Wir konnten diese Session nicht stornieren. Bitte versuche es erneut.",
|
|
@@ -16,6 +31,7 @@
|
|
|
16
31
|
"forge.account_dashboard.cancel_confirm_paid": "Sie wird nicht verlängert, und Ihre Vorteile bleiben bis zum Ende des bezahlten Zeitraums bestehen.",
|
|
17
32
|
"forge.account_dashboard.cancel_confirm_paid_until": "Sie wird nicht verlängert, und Ihre Vorteile bleiben bis zum {date} bestehen.",
|
|
18
33
|
"forge.account_dashboard.cancel_confirm_title": "Mitgliedschaft kündigen?",
|
|
34
|
+
"forge.account_dashboard.cancel_confirm_trial": "Ihre kostenlose Testphase wird nicht zu einer bezahlten Mitgliedschaft. Ihre Vorteile bleiben bis zum {date} bestehen, und es wird nichts berechnet.",
|
|
19
35
|
"forge.account_dashboard.cancel_deletion": "Löschung abbrechen",
|
|
20
36
|
"forge.account_dashboard.cancel_session": "Session stornieren",
|
|
21
37
|
"forge.account_dashboard.cancel_ticket": "Ticket stornieren",
|
|
@@ -29,6 +45,8 @@
|
|
|
29
45
|
"forge.account_dashboard.courses_start": "Starten",
|
|
30
46
|
"forge.account_dashboard.courses_title": "Deine Kurse",
|
|
31
47
|
"forge.account_dashboard.current_password": "Aktuelles Passwort",
|
|
48
|
+
"forge.account_dashboard.cycle_month": "Monat",
|
|
49
|
+
"forge.account_dashboard.cycle_year": "Jahr",
|
|
32
50
|
"forge.account_dashboard.data_export_description": "Exportiere eine Kopie deiner persönlichen Daten.",
|
|
33
51
|
"forge.account_dashboard.data_section_title": "Deine Daten",
|
|
34
52
|
"forge.account_dashboard.delete_section_title": "Konto löschen",
|
|
@@ -50,6 +68,9 @@
|
|
|
50
68
|
"forge.account_dashboard.membership_price": "{amount} / {cycle}",
|
|
51
69
|
"forge.account_dashboard.membership_renews_on": "Verlängert sich am {date}",
|
|
52
70
|
"forge.account_dashboard.membership_title": "Aktuelle Mitgliedschaft",
|
|
71
|
+
"forge.account_dashboard.membership_trial_cancelled": "Gekündigt. Es wird nichts berechnet.",
|
|
72
|
+
"forge.account_dashboard.membership_trial_then": "Danach {amount} pro {cycle} ab dem {date}.",
|
|
73
|
+
"forge.account_dashboard.membership_trial_until": "Kostenlose Testphase bis zum {date}",
|
|
53
74
|
"forge.account_dashboard.new_password": "Neues Passwort",
|
|
54
75
|
"forge.account_dashboard.notifications_title": "Benachrichtigungsarten",
|
|
55
76
|
"forge.account_dashboard.notify_chat_description": "Werde benachrichtigt, wenn neue Chatnachrichten ankommen",
|
|
@@ -1003,8 +1024,16 @@
|
|
|
1003
1024
|
"forge.film_watch.up_next": "Als Nächstes: {title}",
|
|
1004
1025
|
"forge.film_watch.window_not_started": "Dein Zeitfenster startet jetzt.",
|
|
1005
1026
|
"forge.forge_stripe_payment.authorize": "{amount} vormerken",
|
|
1027
|
+
"forge.forge_stripe_payment.cycle_month": "Monat",
|
|
1028
|
+
"forge.forge_stripe_payment.cycle_year": "Jahr",
|
|
1006
1029
|
"forge.forge_stripe_payment.deferred_charged_on_approval": "Deine Karte wird nur belastet, wenn wir deine Anfrage freigeben.",
|
|
1007
1030
|
"forge.forge_stripe_payment.deferred_hold_on_statement": "Bis dahin erscheint eine vorübergehende Vormerkung auf deiner Abrechnung.",
|
|
1031
|
+
"forge.forge_stripe_payment.setup_charged_on_approval": "Jetzt wird nichts abgebucht. Wir belasten diese Karte nur, wenn wir deine Bewerbung annehmen, und dann mit dem angezeigten Betrag ({amount}).",
|
|
1032
|
+
"forge.forge_stripe_payment.setup_failed": "Wir konnten deine Karte nicht speichern. Bitte versuche es erneut.",
|
|
1033
|
+
"forge.forge_stripe_payment.setup_incomplete": "Deine Karte wurde nicht gespeichert. Bitte versuche es erneut.",
|
|
1034
|
+
"forge.forge_stripe_payment.start_free_trial": "Kostenlos testen",
|
|
1035
|
+
"forge.forge_stripe_payment.submit_application": "Bewerbung absenden",
|
|
1036
|
+
"forge.forge_stripe_payment.trial_consent": "Heute wird nichts abgebucht. Ab dem {date} {amount} pro {cycle}. Wenn du vorher kündigst, wird dir nichts berechnet.",
|
|
1008
1037
|
"forge.forgot_password_form.back_to_login": "Zurück zur Anmeldung",
|
|
1009
1038
|
"forge.forgot_password_form.email_label": "E-Mail",
|
|
1010
1039
|
"forge.forgot_password_form.email_placeholder": "E-Mail",
|
|
@@ -1082,8 +1111,27 @@
|
|
|
1082
1111
|
"forge.login_form.verify_submit": "Bestätigen & anmelden",
|
|
1083
1112
|
"forge.login_form.verify_title": "Bestätige deine E-Mail",
|
|
1084
1113
|
"forge.login_form.verifying": "Wird geprüft…",
|
|
1114
|
+
"forge.membership_checkout.accept_invite": "Einladung annehmen",
|
|
1085
1115
|
"forge.membership_checkout.amount_below_minimum": "Bitte {minimum} oder mehr eingeben.",
|
|
1086
1116
|
"forge.membership_checkout.amount_not_positive": "Bitte einen Betrag größer als null eingeben.",
|
|
1117
|
+
"forge.membership_checkout.answer_required": "Bitte beantworte: {question}",
|
|
1118
|
+
"forge.membership_checkout.application_approved_body": "Deine Bewerbung für {tier_name} wurde angenommen. Deine Mitgliedschaft wird eingerichtet.",
|
|
1119
|
+
"forge.membership_checkout.application_approved_title": "Du wurdest angenommen",
|
|
1120
|
+
"forge.membership_checkout.application_card_title": "Deine Karte",
|
|
1121
|
+
"forge.membership_checkout.application_closed_body": "Deine Bewerbung für {tier_name} ist geschlossen. Es wurde nichts abgebucht.",
|
|
1122
|
+
"forge.membership_checkout.application_closed_title": "Bewerbung geschlossen",
|
|
1123
|
+
"forge.membership_checkout.application_declined_body": "Deine Bewerbung für {tier_name} wurde nicht angenommen. Es wurde nichts abgebucht.",
|
|
1124
|
+
"forge.membership_checkout.application_declined_title": "Bewerbung nicht angenommen",
|
|
1125
|
+
"forge.membership_checkout.application_received_body": "Danke für deine Bewerbung für {tier_name}. Wir schreiben dir eine E-Mail, sobald wir sie geprüft haben.",
|
|
1126
|
+
"forge.membership_checkout.application_received_paid_body": "Danke für deine Bewerbung für {tier_name}. Es wurde nichts abgebucht. Wenn wir deine Bewerbung annehmen, werden {amount} {cycle} von deiner Karte abgebucht und deine Mitgliedschaft beginnt.",
|
|
1127
|
+
"forge.membership_checkout.application_received_title": "Bewerbung eingegangen",
|
|
1128
|
+
"forge.membership_checkout.application_state_approved": "Du wurdest angenommen. Wir richten deine Mitgliedschaft gerade ein.",
|
|
1129
|
+
"forge.membership_checkout.application_state_complete": "Du wurdest angenommen, aber wir konnten deine Karte nicht belasten. Schließe deine Mitgliedschaft unten ab.",
|
|
1130
|
+
"forge.membership_checkout.application_state_declined": "Deine Bewerbung wurde nicht angenommen.",
|
|
1131
|
+
"forge.membership_checkout.application_state_pending": "Deine Bewerbung wird geprüft.",
|
|
1132
|
+
"forge.membership_checkout.application_tier_full_body": "Der letzte Platz wurde vergeben, während deine Karte gespeichert wurde, daher wurde deine Bewerbung geschlossen. Es wurde nichts abgebucht.",
|
|
1133
|
+
"forge.membership_checkout.application_tier_full_title": "{tier_name} ist voll",
|
|
1134
|
+
"forge.membership_checkout.apply": "Bewerben",
|
|
1087
1135
|
"forge.membership_checkout.back": "Zurück",
|
|
1088
1136
|
"forge.membership_checkout.billing_cycle_label": "Abrechnung",
|
|
1089
1137
|
"forge.membership_checkout.billing_monthly": "Monatlich",
|
|
@@ -1093,10 +1141,18 @@
|
|
|
1093
1141
|
"forge.membership_checkout.billing_yearly_price": "Jährlich ({amount})",
|
|
1094
1142
|
"forge.membership_checkout.billing_yearly_pwyw": "Zahle was du willst ab {amount}/Jahr",
|
|
1095
1143
|
"forge.membership_checkout.choose_title": "Mitgliedschaft wählen",
|
|
1144
|
+
"forge.membership_checkout.complete_membership": "Mitgliedschaft abschließen",
|
|
1096
1145
|
"forge.membership_checkout.confirm_change": "Wechsel bestätigen",
|
|
1097
1146
|
"forge.membership_checkout.current_plan": "Aktueller Tarif",
|
|
1098
1147
|
"forge.membership_checkout.empty_title": "Keine Mitgliedschaften verfügbar",
|
|
1148
|
+
"forge.membership_checkout.field_required": "Dieses Feld ist erforderlich",
|
|
1099
1149
|
"forge.membership_checkout.free": "Kostenlos",
|
|
1150
|
+
"forge.membership_checkout.go_to_account": "Zu deinem Konto",
|
|
1151
|
+
"forge.membership_checkout.invite_dead_body": "Sie ist vielleicht abgelaufen, wurde schon verwendet oder durch eine neuere Einladung ersetzt. Frag uns nach einer neuen.",
|
|
1152
|
+
"forge.membership_checkout.invite_dead_title": "Diese Einladung ist nicht mehr gültig",
|
|
1153
|
+
"forge.membership_checkout.invite_email_mismatch": "Du bist als {signed_in_email} angemeldet. Melde dich mit {email} an, um diese Einladung anzunehmen.",
|
|
1154
|
+
"forge.membership_checkout.invite_for_email": "Diese Einladung gilt für {email}.",
|
|
1155
|
+
"forge.membership_checkout.invite_title": "Du bist eingeladen, {tier_name} beizutreten",
|
|
1100
1156
|
"forge.membership_checkout.more_benefits": "+{count} weitere",
|
|
1101
1157
|
"forge.membership_checkout.no_tier_body": "Wähle eine Mitgliedschaft, um fortzufahren.",
|
|
1102
1158
|
"forge.membership_checkout.on_this_plan": "Du hast diesen Tarif",
|
|
@@ -1108,15 +1164,28 @@
|
|
|
1108
1164
|
"forge.membership_checkout.pwyw_charged_as": "Ihnen werden {amount} berechnet.",
|
|
1109
1165
|
"forge.membership_checkout.pwyw_label": "Wie viel möchtest du zahlen?",
|
|
1110
1166
|
"forge.membership_checkout.pwyw_min": "(mindestens {amount})",
|
|
1167
|
+
"forge.membership_checkout.questions_title": "Ein paar Fragen",
|
|
1168
|
+
"forge.membership_checkout.select_placeholder": "Auswählen…",
|
|
1111
1169
|
"forge.membership_checkout.select_tier": "Diesen Tarif wählen",
|
|
1170
|
+
"forge.membership_checkout.start_free_trial": "Kostenlos testen",
|
|
1112
1171
|
"forge.membership_checkout.submit": "Abonnieren",
|
|
1113
1172
|
"forge.membership_checkout.submitting": "Wird verarbeitet…",
|
|
1114
1173
|
"forge.membership_checkout.title": "{tier_name} beitreten",
|
|
1115
1174
|
"forge.membership_checkout.total": "Gesamt",
|
|
1175
|
+
"forge.membership_checkout.trial_card_title": "Starte deine kostenlose Testphase",
|
|
1176
|
+
"forge.membership_checkout.trial_line": "{days} Tage kostenlos testen. Die erste Abbuchung erfolgt am {date}.",
|
|
1116
1177
|
"forge.membership_checkout.view_memberships": "Mitgliedschaften ansehen",
|
|
1117
1178
|
"forge.membership_tier_callout.benefits_label": "Vorteile:",
|
|
1179
|
+
"forge.membership_tier_callout.invite_for_email": "Diese Einladung gilt für {email}. Melde dich mit dieser Adresse an oder erstelle damit ein Konto.",
|
|
1118
1180
|
"forge.membership_tier_callout.join_title": "{tier_name} beitreten",
|
|
1119
1181
|
"forge.membership_tier_callout.more_benefits": "+{count} weitere Vorteile",
|
|
1182
|
+
"forge.membership_tiers.accept_invite": "Einladung annehmen",
|
|
1183
|
+
"forge.membership_tiers.application_approved": "Angenommen, wird eingerichtet",
|
|
1184
|
+
"forge.membership_tiers.application_declined": "Nicht angenommen",
|
|
1185
|
+
"forge.membership_tiers.application_payment_failed": "Angenommen: Mitgliedschaft abschließen",
|
|
1186
|
+
"forge.membership_tiers.application_pending": "Bewerbung wird geprüft",
|
|
1187
|
+
"forge.membership_tiers.apply": "Bewerben",
|
|
1188
|
+
"forge.membership_tiers.complete_membership": "Mitgliedschaft abschließen",
|
|
1120
1189
|
"forge.membership_tiers.empty_title": "Keine Mitgliedschaften verfügbar.",
|
|
1121
1190
|
"forge.membership_tiers.heading": "Mitgliedschaft",
|
|
1122
1191
|
"forge.membership_tiers.price_free": "Kostenlos",
|
|
@@ -1125,6 +1194,11 @@
|
|
|
1125
1194
|
"forge.membership_tiers.price_pwyw_yearly": "Zahle was du willst ab {amount}/Jahr",
|
|
1126
1195
|
"forge.membership_tiers.price_yearly": "{amount}/Jahr",
|
|
1127
1196
|
"forge.membership_tiers.select": "Wählen",
|
|
1197
|
+
"forge.membership_tiers.start_free_trial": "Kostenlos testen",
|
|
1198
|
+
"forge.membership_tiers.trial_badge": "{days} Tage kostenlos testen",
|
|
1199
|
+
"forge.membership_tiers.withdraw": "Bewerbung zurückziehen",
|
|
1200
|
+
"forge.membership_tiers.withdraw_error": "Wir konnten deine Bewerbung nicht zurückziehen. Bitte versuche es erneut.",
|
|
1201
|
+
"forge.membership_tiers.withdrawing": "Wird zurückgezogen…",
|
|
1128
1202
|
"forge.offer_button.buy_now": "Jetzt kaufen",
|
|
1129
1203
|
"forge.offer_button.confirm_email": "E-Mail bestätigen",
|
|
1130
1204
|
"forge.offer_button.continue_to_payment": "Weiter zur Zahlung",
|